【WWDC2017】iOS開発におけるEmbedded Framework活用法 + ラボノススメ
井原岳志
こんにちは。井原 (@nonchalant0303) です。スタディサプリENGLISHのiOSエンジニアをやっています。
今回はEmbedded Frameworkを活用したiOSアプリの設計についてご紹介します。
ちなみにですが、2017年6月5日 ~ 9日、アメリカのサンノゼで開催されたWWDC2017に参加してきました。WWDCではKeynoteをはじめとしたセッションの他に、Appleで働く様々な分野のエンジニアとディスカッションをできる『ラボ』というブースがあります。Embedded Frameworkでの設計についていろいろと質問することが出来たので、そこで得た知見も併せてご紹介します。
はじめに
現在、開発しているiOSアプリでは以下のような設計を採用しています。
上記の設計には『下のレイヤーは上のレイヤーのことを知らない』という原則があります。しかし、SwiftではPackage単位ではなくModule単位で名前空間が管理されているので、同一のModuleに複数のレイヤーが含まれていると下のレイヤーの人が上のレイヤーの人を呼び出すことを機械的に防ぐことは出来ません。この問題を解決するためにはレイヤー毎に名前空間が管理されることで防げます。これを実現するためにはいくつかの方法があります。
レイヤー毎に名前空間を管理
1. Nested Types
Nested Typeとはclassやstructの内部に定義されたclassやstructのことを指します。
class Domain {
...
class UseCase {
}
}
Domainレイヤーの外からUseCaseクラスにアクセスするには、Domain.UseCaseのような記述をします。これで名前空間のようなものを実現できます。しかし、この方法では同一ファイルが高頻度で変更がされるのでチーム開発では困難な面があります。そこでExtensionを使ってファイルを分けます。これで同一ファイルの変更が多発する問題を解決できます。
class Domain {}
extension Domain {
class UseCase {}
}
しかし、この方法だとExtensionを多用するのでビルド時間が長時間化してしまう問題が発生してしまうため、あまり現実的な方法とは言えません。
2. Embedded Framework
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を使用することで他にもメリットが生まれます。
[Pros] 差分ビルドが働きやすくなる
iOSアプリ開発のあるあるなのですが、1行変更したらフルビルドが走ってしまうという現象があります。しかし、Embedded Frameworkでレイヤーを管理にすることによりクラス間の依存関係が明確になるので差分ビルドが働きやすくなります。
[Pros] 不必要なライブラリを参照できなくなる
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
[Pros] Module毎に言語のMigrationが出来る
Swiftではたびたび破壊的な変更がなされてきました。その際のMigration作業はかなり時間と集中を要するため、iOSエンジニアの負担になります。しかし、Swift3.2とSwift4では共存が出来るので、**Migration作業がModule毎に行うことが可能になりました。これにより一度にすべてのコードをMigrationせずに済むので、『新機能の追加開発がペンディングしてしまう』といったケースを軽減できます。
[Cons] Main Interfaceなどの仕組みが扱いづらい
StoryboardはViewを管理しているので、Presentationレイヤーに属します。しかし、EntryのStoryboardをDeployment InfoのMain Interfaceで設定する際にPresentation層に属するStoryboardを参照できません。また、このStoryboardからStoryboard Referenceで遷移するStoryboardにも同様の問題が発生します。これはMain TargetのCopy Bundle ResourcesをStoryboard上で参照できないためです。この問題はコードでStoryboard間の遷移を行うか*.storyboard
をMain TargetとPresentationレイヤーに属させると解決します。Embedded Frameworkを活用してレイヤーを分けているのに2つのTargetに属させるのと意味が薄れるのでコードで記述するのが良い気がします。
このように、Main Interface, Storyboard Referenceのような仕組みはEmbedded Frameworkを活用したiOSアプリ単体の開発で扱いづらい面があるので、そもそもこのような用途でEmbedded Frameworkを使うのはAppleの思想と異なるのではないかと考えられます。
WWDCのラボでAppleのエンジニアに訊いてみました
WWDC2017のSwift Open HoursというAppleのエンジニアと話せるラボで質問してきたところ、『Embedded Frameworkを使ってレイヤーを管理する設計は良いアイディアだ』との回答を貰いました。Appleの思想とは大きく外れてはいなさそうです。
しかし、現状はStoryboard上ではBundle ResourcesをModule間で共有できる仕組みがないので、先述のようにコードで記述するかMain TargetとPresentationレイヤーの両方にStoryboardを属させるしかないようです。将来的にそのような仕組みを導入することも検討していると言っていたので、そのうちサポートされるかもしれません。今後の進化に期待ですね。
ラボノススメ
日頃の開発で疑問に思っていることについてAppleのエンジニアと話せたのはとても貴重な体験でした。もし今後WWDCに参加されることがありましたら、ぜひラボに行くことをオススメします!