こんにちは。井原 (@nonchalant0303) です。スタディサプリENGLISHのiOSエンジニアをやっています。
今回はEmbedded Frameworkを活用したiOSアプリの設計についてご紹介します。
ちなみにですが、2017年6月5日 ~ 9日、アメリカのサンノゼで開催されたWWDC2017に参加してきました。WWDCではKeynoteをはじめとしたセッションの他に、Appleで働く様々な分野のエンジニアとディスカッションをできる『ラボ』というブースがあります。Embedded Frameworkでの設計についていろいろと質問することが出来たので、そこで得た知見も併せてご紹介します。
現在、開発しているiOSアプリでは以下のような設計を採用しています。
上記の設計には『下のレイヤーは上のレイヤーのことを知らない』という原則があります。しかし、SwiftではPackage単位ではなくModule単位で名前空間が管理されているので、下のレイヤーの人が上のレイヤーの人を呼び出すことを機械的に防ぐことは出来ません。この問題を解決するためにはレイヤー毎に名前空間が管理されることで防げます。これを実現するためにはいくつかの方法があります。
Nested Typeとはclassやstructの内部に定義されたclassやstructのことを指します。
class Domain { ... class UseCase { } }
Domainレイヤーの外からUseCaseクラスにアクセスするには、Domain.UseCaseのような記述をします。これで名前空間のようなものを実現できます。しかし、この方法では同一ファイルが高頻度で変更がされるのでチーム開発では困難な面があります。そこでExtensionを使ってファイルを分けます。これで同一ファイルの変更が多発する問題を解決できます。
class Domain {}
extension Domain { class UseCase {} }
しかし、この方法だとExtensionを多用するのでビルド時間が長時間化してしまう問題が発生してしまうため、あまり現実的な方法とは言えません。
Embedded Frameworkとはターゲット間でコードを共有する仕組みです。この仕組みを使ってレイヤー毎にEmbedded Frameworkを生成して名前空間を実現します。
実際にデモアプリを作成しましたので、詳しい構成はそちらを見ていただけると助かります。
https://github.com/Nonchalant/ToDo
例えば、PresentationレイヤーでDomainレイヤーを参照したいときはimport Domain
と記述します。
import Foundation import Domain class Presenter { ... private let useCase: UseCase init(useCase: UseCase) { self.useCase = useCase ... } }
別レイヤーを参照する時は明示的にimport文を書く必要があるので、import文の有無で他のレイヤーに不適切に依存しているかどうかを瞬時に理解することができます。また、Embedded Frameworkを使用することで他にもメリットが生まれます。
iOSアプリ開発のあるあるなのですが、1行変更したらフルビルドが走ってしまうという現象があります。しかし、Embedded Frameworkでレイヤーを管理にすることによりクラス間の依存関係が明確になるので差分ビルドが働きやすくなります。
Embedded Frameworkで依存するライブラリをEmbedded Framework毎に管理できます。Podfileの定義で各レイヤーが依存するライブラリを定義できます。
platform :ios, '9.0' swift_version = '3.1' use_frameworks! target 'ToDo' do pod 'RxSwift' pod 'Swinject' pod 'SwinjectStoryboard' target 'Presentation' do // RxSwift, Swinject, SwinjectStoryboard, RxCocoaに依存している pod 'RxCocoa' target 'PresentationTests' do inherit! :search_paths pod 'Quick' pod 'Nimble' pod 'RxTest' end end target 'Domain' do // RxSwift, Swinject, SwinjectStoryboardに依存している target 'DomainTests' do inherit! :search_paths pod 'Quick' pod 'Nimble' pod 'RxBlocking' end end target 'Infrastructure' do // RxSwift, Swinject, SwinjectStoryboard, RealmSwiftに依存している pod 'RealmSwift' target 'InfrastructureTests' do inherit! :search_paths pod 'Quick' pod 'Nimble' pod 'RxBlocking' end end target 'Utility' do // RxSwift, Swinject, SwinjectStoryboard, SwiftyBeaverに依存している pod 'SwiftyBeaver' end end
https://github.com/Nonchalant/ToDo/blob/master/Podfile
Swiftではたびたび破壊的な変更がなされてきました。その際のMigration作業はかなり時間と集中を要するため、iOSエンジニアの負担になります。しかし、Swift3.2とSwift4では共存が出来るので、**Migration作業がModule毎に行うことが可能になりました。これにより一度にすべてのコードをMigrationせずに済むので、『新機能の追加開発がペンディングしてしまう』といったケースを軽減できます。
StoryboardはViewを管理しているので、Presentationレイヤーに属します。しかし、EntryのStoryboardをDeployment InfoのMain Interfaceで設定する際にPresentation層に属するStoryboardを参照できません。また、このStoryboardからStoryboard Referenceで遷移するStoryboardにも同様の問題が発生します。これはMain TargetのCopy Bundle Resourcesに含まれていないためです。この問題を解決するために*.storyboard
をMain TargetとPresentationレイヤーに属させます。これによりMain TargetからStoryboardを参照できます。
しかし、Storyboardのような仕組みはEmbedded Frameworkを活用したiOSアプリ単体の開発で扱いづらい面があるので、そもそもこのような用途でEmbedded Frameworkを使うのはAppleの思想と異なるのではないかと考えられます。
WWDC2017のSwift Open HoursというAppleのエンジニアと話せるラボで質問してきたところ、『Embedded Frameworkを使ってレイヤーを管理する設計は良いアイディアだ』との回答を貰いました。Appleの思想とは大きく外れてはいなさそうです。
しかし、現状はBundle ResourcesをModule間で共有できる仕組みがないので、先述のようにMain TargetとPresentationレイヤーの両方にStoryboardを属させるしかないようです。将来的にそのような仕組みを導入することも検討していると言っていたので、そのうちサポートされるかもしれません。今後の進化に期待ですね。
日頃の開発で疑問に思っていることについてAppleのエンジニアと話せたのはとても貴重な体験でした。もし今後WWDCに参加されることがありましたら、ぜひラボに行くことをオススメします!
※ コメントはこちらのに同意の上、投稿ください。