この記事はVASILY DEVELOPERS BLOGにも同じ内容で投稿しています。よろしければ他の記事もご覧ください。
iOS 10.1 のリリースから遅れること3日、Xcode 8.1 がリリースされました。この Xcode 8.1 では Swift のバージョンが 3.0.1 にアップデートされています。
iQON の iOS アプリでは、Xcode 8 リリース後すぐに Swift 2.3 へのアップデートは済ませたのですが、最近 Swift のバージョンを 2.3 → 3.0.1 にアップデートしました。
本記事は、作業中に対応したエラー修正の記録のようなものです。とても長くなっていますが、Swift 2系 → 3系にアップデートするときの手助けになればと思います。
モチベーション
現在も引き続きSwift 2.3 で開発を続けることはできますが、いずれは Swift 3.x 系へアップデートすることになるでしょう。
一方、 Realm や RxSwift などのメジャーなライブラリは続々と Swift 3.0 の正式対応版をリリースしています。
このまま Swift 2.3 で開発を続けた場合、アップデート時の作業が増えます。
さらに、各OSSライブラリも今後のアップデートは Swift 3.x 系のみということもあるでしょう。
iQONでは、使用しているライブラリが全て Swift 3.0 対応版がリリースされたため、 iQON でも Swift 3.0.1 へのアップデートに踏み切りました。
前提条件
本記事の Swift 3.0.1 アップデート作業は下記の環境においてのものです。
- MacBook Pro (Retina, 15-inch, Mid 2015)
- macOS Sierra 10.12.1
- Swift (2.3 → 3.0.1)
- Xcode (8.0 → 8.1)
- Carthage 0.18.1
- CocoaPods 1.0.1
- サポートiOSバージョン : 9.0 以降
- Objective-C / Swift のハイブリッド (比率は 40% : 60%)
- iPhoneのみ (not universal)
移行作業
大まかな流れは下記のようになっています。
-
Xcode 8 のコンバーターで Swift 2.3 の文法を 3.0.1 式に一括置換
-
CocoaPods, Carthage で管理するライブラリを Swift 3.0.1 に対応したバージョンに載せ替える
-
ビルド時にXcodeに表示されるのエラーを取り除く
〜ここでやっとビルドが通る〜 -
Xcodeに表示されるのワーニングを取り除く - 表示崩れなどのバグ修正 & テストを繰り返す
Swift 2.3 → 3.0.1 のコンバート
Xcode画面上部メニューの Edit → Convert → To Current Swift Syntax...
ライブラリの部分は後ほど対応するので、自分達で管理している部分だけチェックを入れてコンバートを実行します。
私の環境では、コンバートに10〜15分程度かかったと思います。
Carthage
Cartfile
内の各ライブラリのバージョンを Swift 3.0 に対応したものに変更して、 carthage update
するだけです。
CocoaPods
各ライブラリの Build Settings で Swift 3.0.1 を使うようにするために、Podfile に下記のような設定を追加します。
swift_version = '3.0.1'
各ライブラリ
公式に Swift 3.0 移行ガイドを用意してくれているものもあるので、まずは公式情報を確認しましょう。
例えば APIKit では、丁寧な移行ガイドがありました。
APIKit 3 Migration Guide
コンパイルエラー対応
発生した多数のエラーとその対応方法を紹介します。
[Error] String(describing:)
クラス名文字列を取得する処理
Swift 2.3
// Swift 2.3
String(SomeClass)
Xcode 8でコンバート後
String(describing: SomeClass) // コンパイルエラー
Swift 3.0.1 で正しくは
String(describing: SomeClass.self)
[Error] Implicitly Unwrapped Optional が廃止になった影響
Objective-Cで、nonnull
, nullable
の指定がないメソッドやプロパティの型は、Swift 2.3 から扱うとき、Implicitly Unwrapped Optional (IUO) として !
が付いていました。
Swift 3.0 以降では IUO は廃止され、Optional の派生型として扱われます。
Objective-C
typedef void (^ViewHandler)(UIView *);
Swift 2.3
let handler: ViewHanlder = { (view: UIView!) in
view.alpha = 0.5
}
Xcode 8でコンバート後
Swift 3.0.1 では、クロージャーの引数の型が Optional になります。
let handler: ViewHanlder = { (view: UIView?) in
view.alpha = 0.5 // コンパイルエラー。viewはOptionalなので直接alphaにはさわれません
}
Swift 3.0.1 で正しくは
// Swift 3.0
let handler: ViewHanlder = { (view: UIView?) in
view?.alpha = 0.5
}
ドキュメント
[SE-0054] Abolish ImplicitlyUnwrappedOptional type
[Error] NSErrorのプロパティにアクセスできない
Objective-C のメソッドで、コールバックのクロージャーにNSError
を返すメソッドがありました。
Swift 3ではNSError
がError
に変換されるため、NSError
クラスのプロパティが参照できません。
Swift 2.3
failure: { (error: NSError!) in
print("domain: \(error.domain) / code: \(error.code)")
}
Xcode 8でコンバート後
failure: { (error: Error) in
print("domain: \(error.domain) / code: \(error.code)") // コンパイルエラー。NSErrorのプロパティにアクセスできない
}
Swift 3.0.1 で正しくは
failure: { (error: Error) in
let nserror = error as NSError
print("domain: \(nserror.domain) / code: \(nserror.code)")
}
[Error] プロトコルに適合したのメソッド定義がエラーになってしまう
Swift 2.3
下記のコードでも特に問題はありません
class BrandPanelListDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
...
}
extension BrandPanelListDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
Xcode 8 でコンバート後
Objective-C method 'tableView:cellForRowAt:' provided by method 'tableView(_:cellForRowAt:)' does not match the requirement's selector ('tableView:cellForRowAtIndexPath:')
というエラーが発生。
class BrandPanelListDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
...
}
extension BrandPanelListDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // コンパイルエラー
Swift 3.0.1 で正しくは
Xcode のエラーメッセージをクリックすると自動で@objc(...)
が挿入されて、エラーはなくなります。
class BrandPanelListDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
...
}
extension BrandPanelListDataSource {
@objc(tableView:cellForRowAtIndexPath:) func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
もしくは、 extension
ごとにプロトコルを記述しても修正できます。
こちらの方が @objc(...)
の記述が不要なうえ、コードの見通しがよくなると思います。
class BrandPanelListDataSource: NSObject {
...
}
extension BrandPanelListDataSource: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
extension BrandPanelListDataSource: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
....
[Error] CGContextSetLineDash
点線を描くための CoreGraphics
のメソッドが変更されていました。
Swift 2.3
CGContextSetLineDash(context, 0.0, [4], 1)
Xcode 8 でコンバート後
'CGContextSetLineDash' is unavailable: Use setLineDash(self:phase:lengths:)
というエラーが発生します。
Swift 3.0.1 で正しくは
CGContext
のインスタンスメソッドを使います。
context.setLineDash(phase: 0.0, length: [4.0])
[Error] enumerated() の要素のキーワードが変更
enumerated()
の要素にアクセスする際のキーワードが index
→ offset
に変更になりました。
Swift 2.3
objects
.enumerate()
.forEach { print($0.index) }
Xcode 8 でコンバート後
Value of tuple type '(offset: Int, element: Any)' has no member 'index'
というエラーが発生します。
.enumerated()
.forEach { print($0.index) } // コンパイルエラー
Swift 3.0.1 で正しくは
enumerated()
(EnumeratedSequence<Array<Element>>
) の各要素は offset
でアクセスできます
objects
.enumerated()
.forEach { print($0.offset) }
ドキュメント
Migrating to Swift 2.3 or Swift 3 from Swift 2.2
Users may need to manually rename the tuple element index to offset when accessing the result of Collection.enumerated()
[Error] コンバーターのキャスト漏れ
String
の変数に NSString
を代入するときなどに明示的なキャストが必要になりました。
Swift 2.3
var parameters: [String: AnyObject] = [
"key1": "apple",
"key2": "orange",
]
parameters["key3"] = (someBool ? "banana" : "grape")
Xcode 8 でコンバート後
parameters
の値の型に合わせて as AnyObject
を差し込んでくれますが、中途半端です。
var parameters: [String: AnyObject] = [
"key1": "apple" as AnyObject,
"key2": "orange" as AnyObject,
]
parameters["key3"] = (someBool ? "banana" : "grape" as AnyObject) // as AnyObject は "grape" にしかかかっていないのでエラー
Cannot convert value of type 'String' to expected argument type 'AnyObject'
というエラーになります。
Swift 3.0.1 で正しくは
as AnyObject
のかかる範囲を変更します。
let parameters: [String: AnyObject] = [
"key1": "apple" as AnyObject,
"key2": "orange" as AnyObject,
]
parameters["key3"] = (someBool ? "banana" : "grape") as AnyObject
これでも良いのですが、多くのコードに as AnyObject
が入ってしまい、可読性が著しく下がります。
parameters
の型を [String: Any]
に変更すると、 as AnyObject
へのキャストが不要になります。
let parameters: [String: Any] = [
"key1": "apple",
"key2": "orange",
]
parameters["key3"] = (someBool ? "banana" : "grape")
ドキュメント
[SE-0072] Fully eliminate implicit bridging conversions from Swift - swift-evolution
[Error] コンバーターが @escaping
を付けてくれない
『クロージャーを引数に持つクロージャー』を引数に持つメソッドにおいて、コンバーターが自動で @escaping
を付けてくれない事がありました。
(コールバック地獄だということはさておき…)
@escaping
については、弊社ニコラスのブログも参考にしてみてください。
Swift 3の変更点の裏側 (アクセス制御 / @escaping) - VASILY DEVELOPERS BLOG
Swift 2.3
private typealias CompletionType = Void -> Void
...
private func runInBackground(task: CompletionType -> Void) {
Xcode 8 でコンバート後
fileprivate typealias CompletionType = () -> Void
...
private func runInBackground(_ task: @escaping (CompletionType) -> Void) {
CompletionType
のクロージャーも非同期で呼ばれるため、@escaping
が必要ですが、コンバーターは@escaping
を付けてくれません。
そして、 CompletionType
を実行するコードでは、下記のようなエラーが発生します。
Closure use of non-escaping parameter 'completeTask' may allow it to escape
Swift 3.0.1 で正しくは
typealias
宣言時に @escaping
をつけるのはエラー
// Error: "@escaping may only be applied to parameters of function type"
fileprivate typealias CompletionType = @escaping () -> Void
メソッド宣言時に @escaping
を書くのが正しいようです。
fileprivate typealias CompletionType = () -> Void
...
fileprivate func runInBackground(_ task: @escaping (@escaping CompletionType) -> Void) {
ドキュメント
[SE-0103] Make non-escaping closures the default
[Error] UIntのプロパティには数字を直接代入できない
UInt型のプロパティに直接数値リテラルを代入できなくなりました。
Swift 2.3
// maxCacheSize は UInt型
SDImageCache.shared().maxCacheSize = 1024 * 1024 * 512 // 何も問題ない
Xcode 8 でコンバート後
SDImageCache.shared().maxCacheSize = 1024 * 1024 * 512 // エラー
Cannot assign value of type 'Int' to type 'UInt'
というエラーが発生します
Swift 3.0.1 で正しくは
明示的に UInt
のコンストラクタを使うか、 as UInt
でキャストする必要があります。
SDImageCache.shared().maxCacheSize = UInt(1024 * 1024 * 512)
SDImageCache.shared().maxCacheSize = 1024 * 1024 * 512 as UInt
ドキュメント
[SE-0072] Fully eliminate implicit bridging conversions from Swift - swift-evolution
ワーニング対応
発生したワーニングとその対応方法を紹介します。
[Warning] DispatchQueue.GlobalQueuePriority が deprecated
Swift 3.0 から GCD の記述方法が変更になりました。
Xcode 8 のコンバーターが自動で変換してくれますが、変換されたものが deprecated でした。
Swift 2.3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
}
Xcode 8でコンバート後
DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.default).async { // ワーニング
}
しかし、下記のように2つのワーニングが発生します。
`default` was deprecated in iOS 8.0: Use qos attributes instead
`global`(priority:)` was deprecated in iOS 8.0
Swift 3.0.1 で正しくは
DispatchQueue.global(qos: .default).async {
}
// .defaultの場合、引数は省略可能
DispatchQueue.global().async {
}
[Warning] 戻り値があるメソッドの戻り値を使っていない場合はワーニングになる
Swift 2.3
navigationController?.popViewControllerAnimated(true)
Xcode 8 でコンバート後
Expression of type 'UIViewController?' is unused
というワーニングが発生する
navigationController?.popViewController(animated: true)
Swift 3.0.1 で正しくは
明示的に戻り値を使わないように書きます。
_ = navigationController?.popViewController(animated: true)
ドキュメント
[SE-0047] Defaulting non-Void functions so they warn on unused results
その他特に苦労した話
Objective-C クラスのプロパティを文字列に組み込むときの不具合
IUO が廃止されたことで、 Objective-C のモデルクラスが持つプロパティを Swift 3.0 の String リテラル( ""
) に含めると "Optional(1)" のような文字列になってしまいます。
iQONでは、APIのURL文字列でこの問題が多く発生したため、多くのリクエスト処理がエラーになってしまい、対応に苦労しました。
Objective-C
@interface User
@property (strong, nonatomic) NSString *name;
@end
Swift 2.3
user.name = "Bob"
print("name: \(user.name)") // name: Bob
Xcode 8でコンバート後
IUO (!
) は Optional の派生型なので、文字列リテラルに組み込むと "Optional" と表示されてしまいます。
user.name = "Bob"
print("name: \(user.name)") // name: Optional("Bob")
Swift 3.0.1 で正しくは
Optional変数として if let
/ guard let
で明示的にアンラップして変数を扱います。
user.name = "Bob"
if let name = user.name {
print("name: \(name)") // name: Bob
}
ドキュメント
[SE-0054] Abolish ImplicitlyUnwrappedOptional type
APIKit のリクエストパラメータ設定処理
iQONではAPIの通信処理に APIKit を使用しています。
Swift 3.0 に対応した APIKit 3.0.0 から リクエストパラメータを指定するための計算型プロパティの型が変更になりました。
public protocol Request {
// APIKit 2.x
var parameters: AnyObject? {
// APIKit 3.x
var parameters: Any? {
リクエストするURLごとにこの計算型プロパティを実装するのですが、 Xcode 8 のコンバーターはこれを修正してくれないため、自分で対応するしかありません。
この計算型プロパティの型を変更しないと、 Request
protocol の parameters
は実装されていないことになるため、通信時にパラメータが無いことになってしまいます。
この変更に気付かなかったため、アイテム検索のページで検索条件を変更しても何も結果が変わらず、原因がわかるまでかなり苦労しました。
Optional の 大小比較 <
, >
下記のような Optional の値の大小比較が Swift 3.0 ではできなくなりました。
let a: Int? = nil
let b: Int? = 4
print(a < b) // true
そのため、Xcode 8のコンバーターは Optional の大小比較をするコードが存在するファイルごとに 、下記のような不等号演算子を実装するコードを挿入します。
fileprivate func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l < r
case (nil, _?):
return true
default:
return false
}
}
fileprivate func > <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l > r
default:
return rhs < lhs
}
}
この演算子定義のおかげで既存のコードは同じように動作しますが、ファイルごとに宣言されているのは気持ちよくないですし、デフォルトの挙動が変わったのなら、 Optional の大小比較自体をやめるべきだと思います。
let a: Int? = nil
let b: Int? = 4
let result: Bool
if let a = a, let b = b {
result = a < b
} else {
result = false
}
print(result) // false
ドキュメント
[SE-0121] Remove Optional Comparison Operators
private → fileprivate
Xcode 8 のコンバーターは private
, fileprivate
の使い分けが一切ないまますべて fileprivate
に変換してしまいます
どこかのタイミングでスコープが小さい物は private
に変更する予定です。
プッシュ通知用のDevice Token
Swift 3.0 にアップデートしてから、プッシュ通知用のDevice Tokenが今までの方法では取得できなくなってしまいました。
Swift 2.3
let deviceTokenString = deviceToken
.description
.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: "<>"))
.stringByReplacingOccurrencesOfString(" ", withString: "")
// "a8f1ef4e27181279f3b60e2db043f1409211faf6b24382e1a0a1223b797fbc79" のような64文字の文字列
Xcode 8でコンバート後
Data
の description
のレスポンスが変更されたため、必ず 32bytes
という文字列になってしまいます。
let token: String = deviceToken
.description
.trimmingCharacters(in: CharacterSet(charactersIn: "<>"))
.replacingOccurrences(of: " ", with: "")
// "32bytes"
Swift 3.0.1 で正しくは
let token: String = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
// "a8f1ef4e27181279f3b60e2db043f1409211faf6b24382e1a0a1223b797fbc79" のような64文字の文字列
ドキュメント
こちらのQiitaの記事が参考になりました。
Swift 3.0でのプッシュ通知用のDevice TokenのData型から16進数文字列への変換方法
最後に
今回は、Swift 3.0.1 にアップデートする際の対応方法を紹介しました。
これらの対応方法はあくまで、iQONで発生したエラーの対応方法ですので、そのほかのエラーについては、 swift-evolution を参考にするのが良いです。
本記事がSwift 3系へアップデートする際の参考になれば幸いです。