iQONをSwift 3.0.1にアップデートしたときに対応したポイント

f:id:vasilyjp:20180927104633j:plain

iOSエンジニアの庄司 (@WorldDownTown) です。 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 系へアップデートすることになるでしょう。 一方、 RealmRxSwift などのメジャーなライブラリは続々と 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分程度かかったと思います。

f:id:vasilyjp:20161101163654p:plain


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ではNSErrorErrorに変換されるため、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()の要素にアクセスする際のキーワードが indexoffset に変更になりました。

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

f:id:vasilyjp:20161101163625p:plain

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でコンバート後

Datadescription のレスポンスが変更されたため、必ず 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文字の文字列

ドキュメント

こちらのQitaの記事が参考になりました。 Swift 3.0でのプッシュ通知用のDevice TokenのData型から16進数文字列への変換方法


最後に

今回は、Swift 3.0.1 にアップデートする際の対応方法を紹介しました。 これらの対応方法はあくまで、iQONで発生したエラーの対応方法ですので、そのほかのエラーについては、 swift-evolution を参考にするのが良いです。

本記事がSwift 3系へアップデートする際の参考になれば幸いです。

VASILYではiQONを一緒に開発してくれるiOSエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。 https://www.wantedly.com/projects/62340www.wantedly.com

カテゴリー