今、Swift2 で書いている同胞達へ。最低限やっておきたい、Swift2 のままで始める Swift3 対策

  • 120
    いいね
  • 2
    コメント

Xcode8.2 を最後に Swift2 のサポートが終わりますね。Swift3 に移行祭りの季節が近づいてまいりました。

実は、Swift2 のコードを Swift3 に書き換え始めて 1 週間。まだお祭りしてます。

Xcode のマイグレーションツールは暴君です。
標準ライブラリだけでなく、自分たちで定義した変数名や列挙子などなど、問答無用で API デザインガイドラインに則った形に変換します。
完全に置き換えしてくれるならまだしも、修正箇所が多いと変換が中途半端になります。
また、当然、RxSwift などのライブラリの変更部分の修正は自分でやらなくてはなりません。

でも、大丈夫です。今から戦う準備をしておけます。
Swift2 のうちから準備を進めておく利点としては下記が挙げられます。

  • 動作確認しながら作業できます (Swift3 への変換を始めると置き換えが終わるまでコンパイルエラーになるので、動作確認できません)
  • 修正箇所が少なくなるので、マイグレーションツールの精度がよくなります
  • マイグレーションツールの変換による恐怖心が薄れます (マイグレーションツールはそこそこの頻度で誤った変換をするので、差分が多くなると怖いです…)
  • 今から意識しつつ書いておけば、機能追加と並行して進められるので、結果的に工数が減ります。

Swift2 のときからやっておいてよかったこと + やっておけばよかったことの共有をしておきます。(いろいろあった気がするけれど、思い出したら追記します)

ちなみに、こちらは下記の環境です。

  • Swift2.3 -> Swift3
  • Xcode8.1
  • Objective-C と混在
  • Objective-C のライブラリを多数使用
  • RxSwift 使用

参考

とても助かった記事 (一部)

Carthage の Swift 3 対応

Carthage も同様の方針で Swift3 対応を進めているとのことです!
しかも、具体的な作業がリスト化されていてとても参考になります!
(こちらのコメントで教えていただきました🙇)

Swift2 のうちからできること

ネーミングをガイドラインに合わせる

今の段階から、できる限りAPI デザインガイドラインに従っておいた方が良いと思います。
マイグレーションツールは、API デザインガイドラインに則った形に半ば強引に変換します。
自分たちの定義した列挙子も、変数名も。

例えば、下記のような変換がされます。

  • 列挙子が lower case に
  • 真偽値型の変数のプレフィックスに is を付け加える

特に列挙子ですが、定義部分だけ lower case にして、case 文などで使用している箇所を合わせて修正してくれないことが非常に多かったです。(やってくれる場合もありますが、体感的には半分くらいでした)
あと、RawValue を String にして、列挙子名をそのまま rawValue にしてる人は気を付けましょう。rawValue も列挙子名と一緒に変わりますよね…。

legacy function の置き換え

CGRectMakeCGRectGet[MaxX|MaxY|Width|Height] など、Swift3 から廃止される関数の置き換えも Swift2 の時点ですでに可能です。
こちらの記事にお世話になりました。
ちなみに、SwiftLint を走らせると警告が出て置き換えまで支援してくれるので大変便利でした。
(view.frame.size.width -> view.frame.width などの置き換えの支援もしてくれます)

標準ライブラリの型やプロトコルのリネームによる名前被りの回避

対策としては、ネームスペースを明示することです。

例えば、Swift3 で ErrorTypeError にリネームされました。
放置すると、下記のような悲劇が起きます。


Swift2 の段階で、ネームスペース (ここでは、Swift.) を明示しておけば解決しそうです。

struct API {
    enum Error: Swift.ErrorType {
        case hogeFailure
    }
}

RxSwift の Swift3 バージョンへの対策

onNext, onError, onComplete, subscribeNext がなくなっています。
そのため、Swift3 で下記メソッドに書き換えていくことになります。
これらのメソッドは Swift2 版でも使えるので、今の内からこちらを使うようにしておくと後で楽でした。

func `do`(onNext: ((E) throws -> Void)? = nil, onError: ((Swift.Error) throws -> Void)? = nil, onCompleted: (() throws -> Void)? = nil, onSubscribe: (() -> ())? = nil, onDispose: (() -> ())? = nil)
        -> Observable<E> 
func subscribe(onNext: ((E) -> Void)? = nil, onError: ((Swift.Error) -> Void)? = nil, onCompleted: (() -> Void)? = nil, onDisposed: (() -> Void)? = nil)
        -> Disposable
例(Swift2)
Observable.of(hoge)
    .doOn(
        onNext: { /* do somethig */ },
        onError: { /* do somethig */ },
        onCompleted: { /* do somethig */  }
    )
    .subscribe(onNext: { /* do somethig */ })
    .addDisposableTo(disposeBag)

Observable.of(hoge)
    .doOn(
        onNext: { /* do somethig */ }
    )
    .subscribe(onNext: { /* do somethig */ })
    .addDisposableTo(disposeBag)

Objective-C との連携周りの整備

じつはこれが一番厄介な問題かもしれません。
これを怠ると、「UILabel に Optional("100") 円 なんて表示がされるようになっていた」というミスが発生しそうです…。

Objective-C で定義されている nullability アノテーションを指定していないメソッドやプロパティを Swift から呼ぶと、今までは IUO 型([型名]!)で引数や戻り値が渡されていましたが、こちらの変更により Optional 型扱いとなります (IUO は型ではなく、属性として残ります)。
これによる問題の一つは、下記の動作の違いが発生することです。

Swift2
var v: String! = "a" // IUO 型
"\(v)"               // "a"
Swift3
var v: String! = "a" // IUO 属性
"\(v)"               // "Optional("a")"

こちらの記事に詳しく載っています。

これによって必要となった変更は主に 3 つでした。これら全て、Swift2 の時点ではすでに修正可能だったのでやっておけば良かったです…。

  • IUO 型で渡されて暗黙的に unwrap されていた値の unwrap 処理もしくは Optional chaining でのハンドリング
  • block 等で引数の型を IUO 型で明示していた場合の対応
  • nullability アノテーションの指定 (可能な限り)

Swift2 と Swift3 での違い

Objective-C
@interface ParentInObjc : NSObject
- (NSString *)huga;
- (void)hogeWithBlock:(void (^)(NSString *param, NSError *error))block;
@end
Swift2
class ParentInObjc : NSObject {
    func huga() -> String!
    func hogeWithBlock(block: ((String!, NSError!) -> Void)!)
}
Swift3
class ParentInObjc : NSObject {
    func huga() -> String!
    func hoge(_ block: ((String?, Error?) -> Swift.Void)!)
}
呼び出し
#if swift(>=3.0)
    // Swift3
    let returnValue = ParentInObjc().huga()  // String?

    ParentInObjc().hoge { (text: String?, error: Error?) in
        // do something
    }

    ParentInObjc().hoge { (text, error) in
        // do something
    }
#else
    // Swift2
    let returnValue = ClassInObjc().huga()  // String!

    ClassInObjc().hogeWithBlock { (text: String!, error: NSError!) in
        // do something
    }
#endif

Swift2 の段階から可能な対策

nullability アノテーションの指定は可能な限りしておくと良いと思います。
ただ、難しい場合もありますよね。(外部のライブラリであったり…)
ということで、下記はそれ以外の対応策です。

Optional 前提でハンドリングを書く

// let returnValue = ParentInObjc().huga()  // String!
// to
let returnValue: String? = ParentInObjc().huga()

//: unwrap
if let value = returnValue {
    value.capitalizedString
}
// or
guard let value = returnValue else {
    assertionFailure("...")
    return
}
// or
returnValue?.capitalizedString
// or
returnValue!.capitalizedString

Block の引数を Optional として明示しておく

// こうしておく ⭕️
ParentInObjc().hogeWithBlock { (text: String?, error: NSError?) in
    // do something
}

// default 🙅
ParentInObjc().hogeWithBlock { (text: String!, error: NSError!) in
    // do something
}