はじめに
WWDC2019でSwiftUIが発表されたこともあり、Swiftが今後より盛り上がっていく流れを感じますね!
さて、今回は個人で運用しているiOSアプリをObjective-Cからswiftへ完全移行したので、その際のハマりポイントや注意点などざっくりまとめたいと思います。
具体的な方法などはすでに他の方も多く記事にされているような気がするので、今回は考え方や設計的な話をしようかと思います。
アプリ紹介
まず、今回移行したアプリの簡単な紹介をします。
アプリ名 : [ぶくめも -Book Memo-](https://apps.apple.com/jp/app/%E3%81%B6%E3%81%8F%E3%82%81%E3%82%82-book-memo/id466265917)ざっくり言うと、書籍の情報を保存しておくアプリです。
書籍の情報は手動で入力もできますが、楽天BooksAPIを連携して検索することも可能です。
初回リリースは2011年9月29日でiOS4時代からのアプリなので、ゴリゴリのObj-Cアプリです。
データストアにはCoreDataを使用しています。(CoreDataのswift移行は結構しんどかった)
下準備
では、移行作業について記載していきたいと思います。
いきなりコードを書き始めてしまうと大変なことになるので、移行作業前にチェックしておくことをまとめます。
作業対象の洗い出し
移行対象を洗い出します。
- Xcodeのプロジェクト設定
- 実装ファイル全般
- 依存関係の洗い出しをします。
- 外部ライブラリ
- 古いライブラリだとサポートが切れていたり、swift化されていなかったりするので、その場合は別のライブラリに乗り換えることも検討する必要があります。
- 乗り換えを行う場合は、この時点である程度目星をつけておくと良いと思います。
- Unitテストなど
- 今回のアプリはUnitテストを書いていないため、説明は割愛します。(-_-)
不要ファイル、処理の削除
移行作業を行う前に不要なファイルや、使っていない処理などは削除します。
リファクタリングもする余裕があればしておきたいですが、swiftに移行しながらでも良いと思いますのでお好みでどうぞ。
なるべく無駄な作業を減らすために、移行前に綺麗にしておいた方が良いです。
また、別章で記載しますが、移行時に一番気をつけておくこととして、Obj-Cファイルの依存関係になるため、不要な#import
文や、.h
ファイルでインポートする必要のないものは.m
ファイルに移動するなどしておいた方が良いです。
モチベーションの維持
これは経験談ですが、移行する前はやる気に満ちていましたが、いざ作業を開始してみるとモチベーションの維持が大変でした。
アプリにもよると思いますが、全てのソースコードを移行するための膨大な作業量と、その結果のコスパの悪さがモチベーションを奪っていきます。
モチベーションを下げる要因
- ひたすら写経
- 数が膨大
- アプリの動作は変わらない
モチベーションが上がる要因
- ファイルが減っていく
- ソースコードが減っていく
- 処理が簡潔になっていく
今回は約1万行のソースコードに対して、1日4~5時間程度の作業で10日ほどかかりました。
まあ、自分のペースでのんびりやるのが良いと思いますが、参考にしていただけると幸いです。
移行作業
それでは、準備も整ったところで、移行作業に取り掛かっていきたいと思います。
流れとしては、
-
- Xcodeの設定変更
-
- 実装ファイルのswift化 + 外部ライブラリの変更
- ライブラリの乗り換えなどを行う場合は、使用している実装ファイルを移行するタイミングで切り替えてしまうのが良いと思います。
- 先にライブラリをswift化してしまうとObj-Cから参照するための処理を用意する必要がある場合があるため、同時か後の方が良いです。
- 3.動作確認(単体)
- 4.動作確認(結合)
2と3を1ファイル単位でひたすら繰り返していく感じになります。
1. Xcodeの設定変更
Xcodeの設定変更はそれほど多くはないですが、
- Objective-C Bridging Headerの追加
-
New File
からヘッダーファイルの追加 -
Build Settings
のObjective-C Bridging Header
にヘッダーのパスを追加
-
- Swiftバージョンの指定
-
Build Settings
のSwift Language Version
に使用するswiftバージョンを設定
-
-
main.h
とmain.m
ファイルを削除(AppDelegateをswift化したあと) - Frameworksフォルダに追加しているライブラリの削除(ライブラリを使用しているソースコードをswift化したあと)
- 明示的にリンクしているライブラリは削除せずにそのまま
2. 実装ファイルのswift化 + 外部ライブラリの変更
ここからが本題です。ここでの作業内容としては、
- 実装ファイルのswift化
- 外部ライブラリの変更、または移行(必要に応じて)
- 動作確認
- 確認は1クラス(1ファイル)ごとに行うのが望ましいです。(こまめに確認)
- 依存関係によっては複数ファイル更新してからでないとビルドが通らないケースもあるかもしれません。その場合、影響範囲が大きくなる場合があるため、動作確認は慎重に行いましょう。
以下swift化をする上で気をつける点を記載します。
ファイルの依存関係を考慮する
下の図はざっくりですが、今回のアプリでのクラスの依存関係になります。
矢印の方向に参照を行なっています。
作業する順番としては、被参照が少ないクラスから行うのが良いと思います。
例えば、被参照が多い、ModelsやUtilityなどから作業を行なった場合、Obj-Cからswiftを参照される場合、@objc
アノテーションを付与しなければならないため、移行後に削除する手間がかかります。
また、Obj-C側からswiftを参照するために、import "HogeApp-Swift.h"
をインポートする必要があるため、面倒です。
Prefix.pch
に記載すれば良いかもしれませんが、動作確認する場合にどっちのクラスが参照されているかわからなくなるため、得策ではありません。
そのため、被参照が少ないクラスから(図で言うとViewControllerから)作業を行えば、これらの作業は最小限に抑えられます。
クラス名を同じにするか、変更するか
こちらは好みの問題になりますが、同じクラス名にするか違うクラス名にするかで対応が変わってきます。
同じクラス名にする場合は、クラスが参照される状況によってクラス名が重複するためビルドエラーになります。
Bridging-Header
で既存のObj-Cクラスをimportしている場合はそれを削除すれば良いです。Obj-Cクラスからimportしている場合、swiftかObj-Cクラスのどちらかの参照を削除します。
ただ、同じクラス名にしたいけど、依存関係が根深くてimportの削除だけではどうにもならない場合は、一時的にObj-C側のクラス名を変更する方法が良いと思いました。(動作確認したら削除するだけなので)
外部ライブラリを変更するかどうか
ライブラリを変更する際も依存関係に注意しましょう。
今回、JSONファイルのパースをもともとNSJSONSerialization
で行なっていましたが、これを期にUnbox
と言う外部ライブラリに変更しました。
-
Unbox
- 今見たらDeprecatedになってました。
Codable
を使えってことみたいですね。
- 今見たらDeprecatedになってました。
これによってViewControllerとUtility,Modelへの変更が必要になってしまったため、割と影響が大きくなってしまいました。
あとは、バーコードの読み取りにZBar
というライブラリを使用していましたが、とうの昔にサポートが切れていたので、AVFoundation
に乗り換えました。
あと、swiftでCocoaPodsライブラリを利用する際はPodfile
のuse_frameworks!
のコメントアウトを外し忘れないように。
動作確認ができるまでは元の実装ファイルは削除しない
実装ファイルのswift化にあたり、方法としてはいくつかあります。
1. .h
ファイルを削除し、.m
ファイルを.swift
に変更して作業する
2. .swift
ファイルを新規追加して作業する
今回は2の方法で作業を行いました。
動作確認をしてて挙動が変わってしまっていたり、リファクタしながらやったら動作が変わってしまったなど、バグが混入することがあります。
その際に元の実装と見比べる必要がありますが、ファイルを消してしまうと確認に時間がかかってしまうため、動作確認が終わるまでは元ファイルは残しておいた方が良いです。
止むを得ず消す場合は、Remove Reference
に留めておいて、終わったらファイルを消すなどが良いかもしれません。
Optional型に注意
Obj-C側でnullable
,nonnull
などの設定を行なっていない場合、swiftからObj-Cを参照する場合、Forced unwrappingされた状態になります。
逆に、Obj-C側で作成したカスタムコンストラクタ(イニシャライザ)はなぜかOptional型で返ってきます。
Forced unwrappingだとnilが入ってくる可能性があるため、unwrapしておく必要があります。
最終的にOptionalでなくなった場合はunwrapしている箇所にwarningが出るのでそのタイミングで消してしまえば良いです。
逆の場合はビルドエラーになるだけなので、どちらでも良いとは思いますが、動作確認中にクラッシュする可能性を考えるとunwrapの方が手間的に良いかもしれません。
Objective-C時代のクラスの扱いに注意
NSDataやNSArrayなどNS~系のクラスはswiftになってからはswift用にインタフェースが変更されたクラスを使用することを推奨されています。
そのため、変更する際は既存で使用していた関数などが使えなくなる場合があります。
例えば、NSMutableArrayには配列をソートするための、open func sort(using sortDescriptors: [NSSortDescriptor])
と言う関数がありますが、swiftのArrayには存在せず、public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
を使用することになります。
また、swift用のインターフェース存在せず、Obj-Cとの関連が根強いクラスもまだ存在しており、CoreDataがそれに当てはまるのではないかと思います。
CoreData周りのクラスや処理は依然として昔のままであり、インターフェースもswiftと相性が良いとは言えません。
(放置されているのか、見捨てられているのか。。。。)
CoreDataModelのSubClassもアノテーションだらけでObj-Cとの結合が強いですね。
@objc(History)
class History : NSManagedObject {
@NSManaged var setDate : Date
@NSManaged var text : String
@NSManaged var author : String
@NSManaged var publisher : String
@NSManaged var isbn : String
}
あと、Relationを管理するためXcodeから自動生成されていたCoreDataGeneratedAccessors
もswiftでは動作しないようです。(自分の調査が甘いかもしれませんが)
そのため、Relationを管理する処理を自前で実装しました。
func addDetail(_ detail : Detail) {
detail.book = self
self.detail.insert(detail)
}
func removeDetail(_ detail : Detail) {
detail.book = nil
self.detail.remove(detail)
CoreDataModels.shared.managedObjectContext?.delete(detail)
}
CoreDataのオワコン感を割と強く感じたので、可能であればRealm
あたりに乗り換えられると良いですが、ユーザーデータを扱う場合は慎重に考える必要がありますね。
3.動作確認(単体)
1ファイルの移行が終わったら、動作確認します。
ここで確認しておくこととして、クラスの参照先に注意してください。
ブレークポイントを置くなどして、swiftファイルが確実に実行されているか確認します。
動作確認していて、ちゃんと動いてると思ったらimportを切り替え忘れていてObj-C側のクラスを参照したまま確認してしまい、
問題ないと思ってファイルを削除したらエラーだらけになったことがあります。
4.動作確認(結合)
全ての移行が完了したら、全体の動作確認を行います。
可能であれば全ての動作パターンは試しておきたいですね。
CoreData周りの修正を行なった際に、Relationの処理が甘く、データは保存されるもののリレーション情報がうまく保存されず、アプリを再起動したらデータが無くなってる(消えてるわけではなく、子供のテーブルに親のIDが保存されていなかった)ことがありました。
あとは、ポインタ渡しで処理していた箇所を参照だけ渡すようにしたことによってデータがうまく引き継がれなかったり。
意外と初歩的なミスが結構見つかります。w
終わりに
自分のアプリをswiftに移行した際のハマりポイントをまとめました。
これから移行を考えている方の参考になれば幸いです。
ちなみにソースコードはこれくらい減りました。
(ライブラリ、xcodeprojなどのファイルを除いた実装ファイルのみ)
Obj-C : 10495行 (.hと.mファイルの合計)
↓
swift : 8784行