Edited at
NIFTYDay 5

俺たちのSwift 3はまだ始まったばかりだ! - ObjCからSwift 3へ移行するときのTips

More than 1 year has passed since last update.

NIFTY Advent Calendar 2016 の5日目の記事です。

昨日は @ykokw さんの「タイマー + スクリプト + mobile backend = プッシュ通知の定期配信」でした。

※もともと「mBaaSでアプリのアクセスカウンターをつくった(仮)」というテーマでしたが、

 mBaaSの基本的な使い方に終始しそうだったので、内容を大幅に変更しています。


まえおき

ニフティ新卒2年目の kiwi です。エンジニアです。

今年の7月から、コンシューマー向けのiOSアプリを担当しています。

ずいぶん前から提供しているため、コードのほとんどがObjective-Cで書かれています。時代を感じます。

しかし秋頃、時代の流れに乗って、Swift移行しはじめることができました:relaxed:

(ちなみにきっかけはiOSDC 2016への参加でした)

新しい機能から徐々にSwiftで書くようにしていますが、Obj-CとSwiftが同居することになるので、いろいろとハマるポイントもあったりします。

この記事ではそんな移行時のポイントと、Swift化してみての感想を書いてみようと思います。


Swift移行時のポイント


$(PRODUCT_MODULE_NAME)-Swift.hは.mファイルでimportする

Swiftで宣言されたクラスをObj-Cで使用する場合に使うヘッダファイルですが、.mのみでインポートします。

.h にインポートし、かつ該当ヘッダをBridgeファイルに記述した場合、コンパイルが通りません。

※追記(2017/1/17)

関数の戻り値や引数としてSwiftで記述されたクラスを使用する(Obj-Cの .h ファイルで書きたい)場合、 @class@protocol を用いて宣言することで、循環参照を防ぎながら記述することができます。

以下、公式ガイドに掲載されているサンプルです。


MyObjcClass.h

// 本来Importが必要なClass,Protocolを記述

@class MySwiftClass;
@protocol MySwiftProtocol;

@interface MyObjcClass : NSObject
- (MySwiftClass *)returnSwiftClassInstance;
- (id <MySwiftProtocol>)returnInstanceAdoptingSwiftProtocol;
// ...
@end



何も考えずに Xcode の指示通りに ! を付けない

Swift の強力な機能であるOptional型、使いこなすとバグを減らすことができていい感じです。

しかし、移行直後に何もわからず書いていると、Xcodeはメソッドの後ろとかで「!をつけよう!」と指示してきます。

この指示は、nilチェックができていない証です。

とはいえ、変数やメソッドに!を付けるとnilが入ったときにクラッシュするだけなので、今までObj-Cで書いていたコードと安全性はさほど変わらないのですが、せっかく注意してくれているので、nilチェックを挟むように検討しましょう。

なお、積極的にguardを使うことで、if letによるネストをある程度抑えることができるのでおすすめです。


Swift 3の命名規則とDelegate

Swift 3では、これまで長ったらしくメソッド名として宣言していた部分を、ラベルとして記述するように推奨されています。

Arrayなどの標準APIについても同様の呼び出し方に変更されています。

var array = ["1", "2", "3"]

// Swift 2
array.removeAtIndex(1)

// Swift 3
array.remove(at: 1)

Swiftでこの命名規則を使ってdelegateを宣言し、それをObj-C側で受ける場合、メソッド名が変わるため注意が必要です。

例えば、Swiftでこのようにdelegateを宣言したとします。

@objc protocol ExampleDelegate {

func didFinishFetch(data: Data)
}

これをObj-Cのソースコードで受け取ろうとすると、こうなります。

@import "project-Swift.h"

@interface SampleViewController <ExampleDelegate> ()
@end

@implementation SampleViewController
- (void) viewDidLoad {
[super viewDidLoad];
}

- (void) didFinishFetchWithData:(NSData *)data {
NSLog(@"%@", data);
}
@end

あら不思議、didFinishFetchというメソッド名が、didFinishFetchWithDataになってしまいました。

このように、Obj-C側ではSwiftで定義したメソッドの、第一引数のラベルをメソッド名の後ろにくっつける形で記述します。

なお、実際にXcodeで開発をする場合、メソッド名を途中まで打つとWithDataの部分まで含めて補完してくれるので、実際はそこまで気にする必要はありません。安心してSwiftに移行しましょう。

ちなみに私の場合、ブランチによってSwift 2と3が混在する(Use Legacy Swift Language Version の設定値が違う)という状態を作り出してしまい、その修正過程の中でDelegateメソッドがうまく呼び出せなくなったことがあります:confounded:


iOS 8+ のメソッド

これはSwift移行とは直接関係ありませんが、ハマったことに変わりはないので自戒の意味をこめて書いておきます。

この世界には、iOS 8以上でしか使えないメソッドが存在します。

そして、Swift 3を扱うことができるXcode 8では、iOS 8以上のみをサポートしています。

他方で、私の担当アプリはまだiOS 7の端末への配信を続けていますので、これらのメソッドを使用するとアプリがクラッシュします。

Xcode 8ではこれらのメソッドでコンパイルエラーが表示されないことがありますので、注意が必要です。

例えば、特定の文字列が含まれているかを確認する contains(_:)(Obj-Cの場合はcontainsString:)などは使えません。

rangeOfString:あたりを使わないといけません。

文字にすると至極当たり前のことではありますが、移行時のエラーと一緒になると混乱します。


移行してみて

Swiftを書き始めてようやく4ヶ月ほどとなりますが、Obj-Cを描くときにセミコロンを忘れて怒られる程度には慣れました。

Optional型は考えて使うことでミスを減らせますし、何よりObj-Cから文字数が減って非常に快適です。

ジェネリクスなど、使いこなせていない機能もまだまだたくさんあります。

今後もSwiftをさらに学びつつ、Swift移行を進め、より快適に開発していきたいと思います。

明日は @tily さんの「hugo で効率的に英語を話す練習ができるサイトを作った話」です。お楽しみに!


参考資料