Swiftならこう書くシリーズ 10選

CouplesアプリのiOSエンジニア、Johnです!

1年前CouplesのObjective-CコードをSwiftに書き換えてから、ベストプラクティスもガンガン変わってきました。それでObjective-CからSwiftに移行する時の10個のTipsにランキングを付けて、10位から紹介させていただきます!

10. 配列の操作ならSwiftの CollectionTypeSequenceType メソッドを使用する

Swiftの ArraySet などは CollectionTypeSequenceType プロトコルを実装していて、いくつかの便利なメソッドが実装されています。
例えば、配列が空かどうかのチェックには、lengthを使わず isEmpty を使うべきです:

Swift Objective-C
if array.isEmpty {
// ...
if (array.length <= 0) {
// ...

配列のindexを操作する時 indices というpropertyを使い、使ったコードを減らすと良いです:
例えば、すべての indexをループする時は:

Swift Objective-C
for i in array.indices {
    // ...
}
for (NSUInteger i = 0; i < array.length; ++i) {
    // ...
}

最初・最後のindexを取得するには:

Swift Objective-C
let firstIndex = array.indices.first
let lastIndex = array.indices.last
NSUInteger firstIndex = 0;
NSUInteger lastIndex = array.length - 1;

最後の n のindex以外を取得するには:

Swift Objective-C
for i in array.indices.dropLast(n) {
    // ...
}
for (NSUInteger i = 0; i < (array.length - n); ++i) {
    // ...
}

最初の n のindex以外を取得するには:

Swift Objective-C
for i in array.indices.dropFirst(n) {
    // ...
}
for (NSUInteger i = n; i < array.length; ++i) {
    // ...
}

このように書くと目的が分かりやすくなり、コードの可読性が上がります。

9. シングルトンの代わりに staticの関数やプロパティを使用する enum で作成する

Objective-Cではstaticのプロパティが書けなかったので、 sharedInstance で実装したシングルトンパターンがよくあります。

Swiftでは、 static varstatic func も書くことができるので、インスタンスを作る必要がありません。

Swift Objective-C
enum GlobalSettings {
    static var isICloudEnabled = false;
    static func saveSettings() {
        // ...
    }
}
@interface GlobalSettings
@property (nonatomic) BOOL isICloudEnabled;
@end

@implementation GlobalSettings

+ (GlobalSettings *)sharedInstance {
    static GlobalSettings *instance;
    static dispatch_once_t once = 0;
    dispatch_once(&once, ^{
        instance = GlobalSettings()
    });
    return instance;
}

- (void)saveSettings {
 // ...
}
@end
if GlobalSettings.isICloudEnabled {
    GlobalSettings.isICloudEnabled = false
    // ...
}
GlobalSettings.saveSettings()
if (GlobalSettings.sharedInstance.isICloudEnabled) {
    GlobalSettings.sharedInstance.isICloudEnabled = NO;
    // ...
}
[GlobalSettings.sharedInstance saveSettings];

空の structclass だとインスタンスの初期化ができてしまいますが、 空の enum ではできません。なので、名前空間やシングルトンを宣言するには enum が良いと思います。

Swift
struct SettingsStruct {}
class SettingsClass {}
enum SettingsEnum {}

let s = SettingsStruct() // OK
let c = SettingsClass() // OK
let e = SettingsEnum() // コンパイルエラー: 'SettingsEnum' cannot be constructed because it has no accessible initializers

8. isKindOfClass() の代わりに is を使用する

isKindOfClass: を使うメリットがなくなりました。 is が短くて、 class 以外にも使えます。

Swift Objective-C
if dictionary["value"] is String {
// ...
if ([dictionary[@"value"] isKindOfClass:[NSString class]]) {
// ...

switchcase にも is 条件が使えます:

Swift
switch dictionary["value"] {
case is String: print("It's a string!")
case is Int: print("It's a number!")
case is NSObject: print("It's an object!")
// ...
}

7. self はクロージャー内でも宣言できる

Objective-Cの経験者は以下の weakstrong パターンをよく使っていると思います:

Objective-C
typeof(self) __weak weakSelf = self;
[session requestWithCompletion:^{
    typeof(self) __strong self = weakSelf;
    [self showDialog];
}];

Objective-Cのブロック内でも変数名を self で宣言できます。これはブロック外の self をキャプチャーさせないためです。

ただ、Swiftではこのような宣言が通常は不可能なので、 strongSelfsSelf などが宣言時によく使用されていると思います。

Swift
session.requestWithCompletion { [weak self] in
    guard let self = self else { // コンパイルエラー
        return
    }
    self.showDialog() // またコンパイルエラー。"self?.showDialog()" で書かないといけない
}

でも実は、以下のように記述することで self の宣言が可能になります。

Swift
session.requestWithCompletion { [weak self] in
    guard let `self` = self else { // OK!
        return
    }
    self.showDialog() // OK! オプショナル不要!
}

6. CGGeometryユーティリティのSwift版を使用する

Objective-Cでは CGGeometry.h にあるグローバル関数や定数を使うのはベストプラクティスになっています。SwiftではそのままCGGeometryのユーティリティが呼べますが、 CGRect, CGSize, CGPoint などにはもっとシンプルなものがextensionで実装されています:

Swift Objective-C
let width = view.frame.width
let bottom = view.frame.maxY
CGFloat width = CGRectGetWidth(view.frame)
CGFloat right = CGRectGetMaxX(view.frame)

CGRectZero, CGRectNull, CGSizeZero, CGPointZero などの定数はSwiftで CGRect.zero, CGRect.null, CGSize.zero, CGPoint.zero で書けます:

Swift Objective-C
let view = UIView(frame: .zero)
UIView *view = UIView(frame: CGRectZero)

昔は CGRect などの関数はグローバルで実装されていて、操作したいstructとその処理のパラメーターを両方引数に渡す必要がありましたが、Swiftではメンバー関数でわかりやすく対応されています:

Swift Objective-C
let isInside = view.bounds.contains(subview.center)
BOOL isInside = CGRectContainsPoint(view.bounds, subview.center)

さらに、比較も簡単になりました。以前は CGSizeEqualToSize(...) のような関数で比較を行っていましたが、Swiftでは == などのオペレータが利用できるようになっています。

Swift Objective-C
if imageView.bounds.size == image.size {
// ...
if (CGSizeEqualToSize(imageView.bounds.size, image.size)) {
// ...

5. Swiftのセッターチェーニングで可変のプロパティが設定できる

Objective-Cではstructのプロパティをチェーニングしても設定できません。Swiftでは普通にできるようになりました:

Swift Objective-C
self.view.frame.origin.y = 20 // OK!
self.view.frame.origin.y = 20; // 何も設定されない!

// You need to do use a temporary variable:
CGRect frame = self.view.frame;
frame.origin.y = 20;
self.view.frame = frame; // OK!

4. do {} で複数行をグルーピングできる

Objective-Cの時は {} でコードをよく整理していました。Swiftでは do 文で同じこともできます。

Swift Objective-C
do {
    let titleLabel = self.titleLabel
    titleLabel.text = "News"
    titleLabel.textColor = .blueColor()
}
do {
    let subtitleLabel = self.subtitleLabel
    subtitleLabel.text = "2016/2/18"
    subtitleLabel.textColor = .greenColor()
}
{
    UILabel *titleLabel = self.titleLabel;
    titleLabel.text = @"News";
    titleLabel.textColor = [UIColor blueColor];
}
{
    UILabel *subtitleLabel = self.subtitleLabel;
    subtitleLabel.text = @"2016/2/18"
    subtitleLabel.textColor = [UIColor greenColor];
}

もう一つのテクニックは、無名なクロージャーで書く方法です:

Swift
lazy var screenImage: UIImage = {

    let largeImage = UIImage(named: "large_image")
    // ...
    let aspectFitImage = // ... resize image
    return aspectFitImage
}()

3. respondsToSelector: の使用をやめて、SwiftのAvailability APIを使用する

Objective-Cではランタイムでメソッドが使えるかどうかを確認するため respondsToSelector: を利用するべきだとよく言われていますが、Swiftではコンパイル時の分析が強化されて、 #available() を使った方が良くなりました。

Swift Objective-C
let manager = CLLocationManager()
if #available(iOS 9, *) {

    manager.allowsBackgroundLocationUpdates = true
}
CLLocationManager *manager = [CLLocationManager new];
if ([manager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {

    manager.allowsBackgroundLocationUpdates = YES;
}

メソッドが存在しないiOSバージョンで呼びだそうとした場合、#available() でラッピングしないとコンパイルエラーが発生します:

Swift
let manager = CLLocationManager()
manager.allowsBackgroundLocationUpdates = true
// コンパイルエラー: 'allowsBackgroundLocationUpdates' is only available on iOS 9.0 or newer

2. 可能な限りジェネリクスやプロトコルで実装する

Swiftのジェネリクスを利用して関数を宣言できると様々な型に使えるようになり、フレキシブルなコードが書けます。例えば、よくある clamp() 関数はこのように書きます:

Swift Objective-C
func clamp<T: Comparable>(minValue: T, _ value: T, _ maxValue: T) -> T {
    return min(max(value, minValue), maxValue)
}

let clampedInt: Int = clamp(1, 155, 100) // = 100

let clampedDouble: Double = clamp(1.0, 155.123 , 100.0) // = 100.0
__attribute__((overloadable))
NSInteger clamp(NSInteger min, NSInteger value, NSInteger max) {  
    return MIN(max, MAX(min, value));
}

__attribute__((overloadable))
double clamp(double min, double value, double max) {  
    return MIN(max, MAX(min, value));
}

NSInteger clampedInt = clamp(1, 155, 100); // = 100

double clampedDouble = clamp(1.0, 155.123, 100.0); // 100.0

ジェネリクスで対応したメソッドはすべてのナンバー型(Int, Double, Floatなど)でそのまま使えます。Objective-Cの時はすべてのオーバロードを実装する必要がありました。

1. Swiftのパターンマッチングをマスターしよう!

Swiftで一番好きな機能はやはりパターンマッチングです。 if 文も、 switch/caseguardforwhilerepeat ループ、そして do/catch のエラーハンドラーも、全部パターンマッチングが使えます。

よく使っている箇所は prepareForSegue(_:) の時です:

Swift Objective-C
switch (segue.destinationViewController, segue.identifier) {
    
case (let controller as SomeViewController, _):
    // ... 処理をする
    
case (let controller as AnotherViewController, "SomeIdentifier"?):
    // ... 処理をする
    
case (let controller as AnotherViewController, "AnotherIdentifier"?):
    // ... 処理をする
    
default:
    break
}
if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]) {

    // ... 処理をする
}
else if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]
    && [segue.identifier isEqualToString:@"SomeIdentifier"]) {
    
    // ... 処理をする
}
else if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]
    && [segue.identifier isEqualToString:@"AnotherIdentifier"]) {
    
    // ... 処理をする
}

この case がとっても便利で、他のところにも使えるようになりました:

Swift
if case (let controller as AnotherViewController) = segue.controller where segue.identifier == "SomeIdentifier" {

    // ... 処理をする
}
// UIButtonじゃないsubviewがスキップされる
for case (let button as UIButton) in self.subviews {

    // ... 処理をする
}
do {

    try removeDatabase(path)
}
catch MyCustomError.Cancelled { // カスタム型

    cancellation()
}
catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError {

    // 無視する
    completion()
}
catch let error as NSError {

    failure(error)
}

パターンマッチングに慣れると、コードの可読性が確実に上がります。

終わりに

Objective-CからSwiftへのコード移行がAppleのおかげでスムーズにできましたが、やはり言語が変わるとベストプラクティスも考え直すべきです。ぜひ、Swiftの新しいAPIや新しいプログラミング思考を使ってみましょう!

エウレカのSwift Style Guideも用意してありますので、ぜひ参考にしてください!

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

いますぐ始める高負荷対策

RaspberryPi×Go言語で電子工作