こんにちは。755新卒iOSエンジニアの林です。
9月にオープンソース化してから初のメジャーリリースとなるSwift3が発表され、755ではiOSアプリのSwift3対応を行いました。
今回のブログでは既存のiOSアプリをSwift3に移行した際に「移行の流れ・気づいた点」などを、社内の幾つかのSwift3移行を始めているプロジェクト(Fresh!/Amebaアプリ/755)からまとめて紹介します。

swift3-width-min

移行の流れ


まずはSwift3移行の流れについて紹介します。
移行の流れの大まかな手順は以下のようになるかと思います。

  • ライブラリ対応
  • Swift3へコンバート
  • エラー削除
  • 正常に動作するよう修正
  • APIをSwift3ガイドラインに合わせる

本ブログではこの中からいくつか取り上げて紹介します。

ライブラリ対応

Swift3対応を行う既存プロジェクトでライブラリを使用している場合、まずそれらのライブラリがSwift3・iOS10に対応しているかの確認が必要になります。
利用しているライブラリがSwift3に非対応の場合、代わりになるライブラリを探すなど対策が必要になります。(Swift製のライブラリは避けて現時点では一時的にObjective-C製のライブラリを使うのも対策になるかもしれません。)githubの各プロジェクトのReleasesでSwift3・Swift2.3・iOS10等の対応状況を確認できるかと思います。

Swift3へコンバート

既存のプロジェクトをXcode8で開くと新しいシンタックスへコンバートを促すアラートが表示されます。Swit3の変更点が多いため自動コンバートでは変換漏れが多く残る印象でした。
この段階ではビルドは全く通らないほどの警告が表示されるため、対応が必要な全てのファイルを目視で確認してエラーを潰してきます。
下記が多く見られたエラーの例になります。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-11-07-%e5%8d%88%e5%be%8c8-18-58

以下で多く見られたエラーを簡単にまとめました。

    • アクセスコントロールについて

      Swift3でpublicやprivateなどのアクセスコントロールが見直されました。privateの扱いが変更されfileprivate,openが追加されたため対応が必要になります。

    • ’NS’prefixの削除

      Objective-C・Apple プラットフォームと関連するものは’NS’prefix を残し、あとはNSを削除する。Swft3の変更にしたがって修正します。

    • enumの要素の書き方が統一

      今まで大文字・小文字でも記述できたenumの要素の頭文字は小文字で統一されました。

    • タイプメソッド扱いしていたものがタイププロパティ扱いに変更

      //Swift2.2
      let blue = UIColor.blueColor()
      UIApplication.sharedApplication()
      dispatch_get_main_queue()
      
      //Swift3.0
      let blue = UIColor.blue
      UIApplication.shared
      DipatchQueue.main
      

      上記の例以外にも変更がされています。

    • GCD周りの変更

      //Swift2.2
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
      //処理
      })
      
      //Swift3.0
      DispatchQueue.global(qos: .default).async {
      //処理
      }
      

      上記の例以外にも変更がされています。

    • Boolインスタンス名にisが追加

      //Swift2.2
      view.hidden = true
      view.userInteractionEnabled = true
      
      //Swift3.0
      view.isHidden = true
      view.isUserInteractionEnable = true
      

      上記の例以外にも変更がされています。

他にも多くの変更エラーが出てくると思いますので、下記URLやまとめ記事等を参考にしてみてください。

気づいたことなど


cocoapodsのバージョン

cocoapodsのバージョンが0.39.0の場合コンバートに時間がかかる可能性があります。
cocoapods-coreのバージョンが1.0.0の場合コンバート時間が短縮できた事例がありました。
現在はcocoapodsのバージョンが1.1.1が使えるため、そちらにアップデートしてからコンバートを行いました。

CoreData周りの変更について

Swift3以前は、NSFetchedResultsController内のFetch結果で扱われる型がAnyObject型でしたが、Swift3からジェネリクス型に変更になりました。
呼び出し側でキャストが不必要になったかわりに、Swift3に移行するにあたってジェネリクスに型を指定する修正が必要でした。

//Swift2.2
var fetchedObjects: [AnyObject]? { get }
func objectAtIndexPath(_ indexPath: NSIndexPath) -> AnyObject
func indexPathForObject(_ object: AnyObject) -> NSIndexPath?
//Swift3.0
//Anyobjectからジェネリクス型のResultTypeに変更された
open var fetchedObjects: [ResultType]? { get }
open func object(at indexPath: IndexPath) -> ResultType
open func indexPath(forObject object: ResultType) -> IndexPath?

NS Prefixの変更点

IndexPathがstructに変更されたことによりSwiftで作成していたNSIndexPathのExtensionが読めなくなりました。
Objective-CではNSIndexPathのExtensionとして、SwiftではIndexPathのExtensionとして使用するため、同名の変数・メソッドを作成し@available(*, unavailable)を用いて場合分けを行いました。StringやURLなど’NS’prefixが削除されたものに対しても対応しました。

//IndexPath.swift
extension IndexPath {
	var sectionRowString: String {
		return "\(section)-\(row)"
	}
}
extension NSIndexPath {
	@available(*, unavailable)
	var sectionRowString: String {
		return (self as IndexPath).sectionRowString
	}
}
//String.swift
extension String {
    static func commaNumberString(_ number: Int) -> String {
        //処理
    }
}

extension NSString {
    @available(*, unavailable)
    class func commaNumberString(number: Int) -> NSString {
        //処理
    }
}

ACAccountStoreのrequestAccessToAccounts()

ACAccountStoreのrequestAccessToAccounts()内にてerrorがnilで返ってきてしまうバグがあるようです。
そのためObjective-CでACAccountStoreのラッパークラスを作成し、それを利用したいSwiftクラスから呼び出すような一時的対応を行いました。

//ACAccountStore+ForSwift.h
#import <Accounts/Accounts.h>

@interface ACAccountStore (ForSwift)

//ForSwift
- (void)requestAccessForSwiftWithAccountType:(ACAccountType *)accountType
                                     options:(NSDictionary *)options
                                  completion:(void (^)(BOOL granted, NSError *error))completioncompletion;
@end
//ACAccountStore+ForSwift.m
#import "ACAccountStore+ForSwift.h"

@implementation ACAccountStore (ForSwift)
- (void)requestAccessForSwiftWithAccountType:(ACAccountType *)accountType
                                     options:(NSDictionary *)options
                                  completion:(void (^)(BOOL granted, NSError *error))completion {
       [self requestAccessToAccountsWithType:accountType options:options completion:completion];
}

@end
//呼び出し側.swift
//requestAccessToAccounts()ではなくrequestAccessForSwift()を使用
self.requestAccessForSwift(with: accountType, options: options) { (granted, error) in
//処理
}

Mutableの変更点

mutable/immutableを表現する方法が、Swift3からURLRequestやDataに対してvarとletで宣言することで表現できるようになりました。
これまではNSMutableURLRequest/NSURLRequestやNSMutableData/NSDataで分けていたため修正が必要になります。

let url = URL(string: "https://github.com/")!

let oldURLRequest = NSURLRequest(url: url)
oldURLRequest.cachePolicy = .reloadIgnoringLocalCacheData 
//再代入できない

let oldMutableURLRequest = NSMutableURLRequest(url: url) 
//class objectなのでmutableなクラスでmutableに
oldMutableURLRequest.cachePolicy = .reloadIgnoringLocalCacheData 
//再代入可能


let newURLRequest = URLRequest(url: url)
newURLRequest.cachePolicy = .reloadIgnoringLocalCacheData 
//再代入できない

var newMutableURLRequest = URLRequest(url: url) 
//structなのでvarにするだけでmutableに
newMutableURLRequest.cachePolicy = .reloadIgnoringLocalCacheData 
//再代入できる

またmutableCopy()はAny型が返ってきていたのでキャストする必要がありましたが、キャストする必要がなくなりました。

let mutableURLRequest = oldURLRequest.mutableCopy() as! NSMutableURLRequest 
var mutableURLRequest3 = newURLRequest

クロージャの変更点

Swift3に変更した際、curried functionの引数ラベル名をつけるとerrorになりました。
引数ラベル名を削除すると、その値の役割を外から判別しにくくなってしまいうまくブラックボックス化できないため、tupleを用いて引数ラベルを残すような対応を行いました。

//swift2.2
func add(arg1: Int) -> (arg2: Int, arg3: Int) -> Int {
    return { arg2, arg3 in arg2 + arg3 }
}  
//error: Function types cannot have argument label 'arg2':use '_' instead

let addThree = add(arg1: 8)
let num = addThree(arg2: 8, arg3: 4)
print(num) 

//swift3.0
func add(arg1: Int) -> ((arg2: Int, arg3: Int)) -> Int {
    return { arg2, arg3 in arg1 + arg2 + arg3 }
}

let addThree = add(arg1: 8)
let num = addThree((arg2: 8, arg3: 4))
print(num)

下記も同様のエラーに対応した例になります。

//swift2.2
typealias AddThreeNumbsers = (v1: Int, v2: Int, v3: Int) -> Int

let add: AddThreeNumbsers = { $0 + $1 + $2 }
let num = add(v1: 8, v2: 8, v3: 4)
print(num)

//swift3.0
typealias AddThreeNumbsers = ((v1: Int, v2: Int, v3: Int)) -> Int

let add: AddThreeNumbsers = { $0 + $1 + $2 }
let num = add((v1: 8, v2: 8, v3: 4))
print(num)

終わりに


今回は既存のiOSアプリをSwift3に移行した際に「気づいた点・移行の流れ」などを、社内の幾つかのプロジェクトからまとめて紹介させて頂きました。
Swift3に移行した際、上記のような修正が必要になるかと思います。ベストプラクティクスではないかもしれませんがこれからSwift3対応される方の手助けになれば幸いです。ありがとうございました。

2016年度新卒入社。株式会社7gogoのiOSを担当。人生で盲腸に3回なった腹痛なりがちエンジニア。