必要ないモジュール依存を省いてSwiftUIのプレビューを快適にした話。
musicLineではマルチモジュールで開発を行っており、それが原因かSwiftUI のプレビューが遅いのが気になっていました。
はじめに
プレビューするとBuild for Previews
となり、プレビューのための再ビルドが走り、数分待つことになります。これが起動時に1回程度だといいのですが、コードを修正した時など?不意にビルドが発生します。あまり頻繁にプレビューで待たされると、デバッグの環境としては良くないですね。
どうやらRealmのモジュールで再ビルドが発生しているようでした。Realmのモジュールは変更してないので、ビルドする必要はないように思えますが。。
また、Realmの謎のエラーでプレビューが失敗したりすることもあり、どうにかしないとダメだと思いました。
そもそもプレビューに必要ないモジュールに依存するのを避けたいです。
そこで、必要な時だけモジュールのクラスを参照し、あとはダミークラスを参照するようにしました。
SwiftでDI
必要な時だけ特定のクラスを参照できる構造へ変更して、クラス使用時にDI(Dependency Injection)で解決します。構造の変更は以下のページを参考にしました。
1. プロトコル(抽象型)を定義
クラスや構造体を参照すると、そこで依存が決定してしまうため、プロトコルを定義します。
protocol Person{ func greet() }
この例では、挨拶する人を抽象的に定義しています。
どんな挨拶をするかは分かりません。
2. クラスや構造体(具体型)を定義
プロトコルに準じて使用したいクラスや構造体を定義します。
struct GoodPerson: Person{ func greet(){ print("Hellow") } }
または
struct BadPerson: Person{ func greet(){ print("...") } }
この例では、挨拶する人を具体的にして「良い人」と「悪い人」を構造体で定義しています。
良い人は「Hellow」、悪い人は「...」(無視)しています。
3. 具体を避けて抽象に依存
依存を回避したいプロパティの部分で、具体的な(クラスや構造体)型の使用を避けて抽象的な(プロトコル)型を使用にします。
struct Room{ let person: Person func visit(){ person.greet() } }
この例では、Room
を定義していますが、Room
にいるperson
はどうのような人物かわからないように定義しています。こうすることで、特定のクラスや構造体に依存しなくても大丈夫になります。
4. 型解決
抽象的な参照は上位層で実際にインスタンスを作成するときに具体的な型を指定します。
let room1 = Room(person: GoodPerson()) room1.visit() let room2 = Room(person: BadPerson()) room2.visit()
この例では、room1
には良い人、room2
には悪い人がおり、部屋を訪れた際の挨拶が変わります。このように、上位層で具体的な型でインスタンス作成することで型解決をして、下位層で必要のない型に依存しなくても良い構造となります。
実装方法
調査した中でSwiftでのDI実装方法として以下の方法がありました。
上記の例のように、1. コンストラクタでインスタンスを渡す
ことが一番簡単ですが、モジュールが多いと、型解決する時に上位層から下位層へインスタンスを渡していく必要があり、影響するクラスが多くなることで可読性が下がります。
また、SwiftでもDIコンテナーのライブラリがあるようで、2. DIコンテナーのライブラリを使う
ことも有効な手段です。
しかし、musicLineではガチガチに抽象化を行い、積極的にDIを使用していく予定もないため、なるべく簡易的でシンプルに使える3. PropertyWrapperを使う
方法を採用することにしました。
あまり依存性を抽象化しすぎると、プロトコルが増えてしまったり、クラスの定義位置にジャンプできなかったりと開発しづらくなると思います。
プロパティラッパーについては以下のページが参考になりました。
おわりに
musicLineのモジュール構成はモジュールの深いところでRealmに依存しないようになりました。
上位層で必要な時にRealmを参照し、SwiftUIプレビューも快適に動きます。