iOS アプリを開発する際は、API などのデータ取得をバックグラウンド処理で行い、画面表示はメインスレッドで処理を行うように実装すると思います※1
※1 UIKit の処理はメインスレッドで実行する必要があるという制約がある。
Use UIKit classes only from your app’s main thread or main dispatch queue, unless otherwise indicated.
https://developer.apple.com/documentation/uikit
画面表示側で DispatchQueue.main.async { }
で囲んでメインスレッドに変換していましたが、モヤモヤしていました。
iOS の制約の UIKit はメインスレッドで実行する必要があるのは機能とは関係もありませんし、メモリからデータ取得や Mock からデータ取得した場合そもそもメインスレッドに変換する必要はありません。
マイナスが付いているのが問題の箇所です。
func viewDidLoad() {
userDetailInteractor.fetchList(userName: userName) { [weak self] result in
switch result {
case let .success(repositories):
- DispatchQueue.main.async {
- self?.view?.reload(list: repositories)
- }
case let .failure(error):
- DispatchQueue.main.async {
- self?.view?.showError(title: error.localizedDescription, message: nil)
- }
}
}
}
【Swift】インスタンスの生成と利用の分離、Composition Root という考えについての記事から、解決策を学ばさせてもらいましたので記録に残しておきます。
今回問題を解決するために、Decorator パターンを利用します。
Decorator パターン
特徴
https://www.techscore.com/tech/DesignPattern/Decorator.html/ より引用。
Decorator パターンを利用した設計では、拡張機能部分のみを持たせた別クラスを用意し、 そのクラスのインスタンス変数に、拡張対象となるインスタンスを持たせ、 拡張対象と同じインタフェースを実装させます。
特徴を箇条書きにすると
- 拡張クラスのインスタンス変数に対象クラスを保持
- 拡張機能部分のみを持たせた別クラスを用意
- 拡張クラスは対象クラスと同じインターフェイス
Java のサイトの実装は、Decorator クラスだけで実現していますが、今回の Swift は Decorator クラス+ Extension の拡張で実現しています。
拡張クラスのインスタンス変数に対象クラスを保持
Decorator クラス
final class DispatchMainQueueDecorator<T> {
let decoratee: T
init(_ decoratee: T) {
self.decoratee = decoratee
}
/// Run on main thread
/// - Parameter completion: processing of a wrapped class
func dispatch(completion: @escaping () -> Void) {
guard Thread.isMainThread else {
return DispatchQueue.main.async(execute: completion)
}
completion()
}
}
コンストラクタの引数として Generics で T 型を指定します。特定のクラスに縛られないので良いですね。対象クラスをインスタンスとして保持しています。
拡張機能部分のみを持たせた別クラスを用意
メインスレッドに変換するのが今回の Decorator の目的なのでその機能だけをクラスに持たせていますね。
func dispatch(completion: @escaping () -> Void) {
guard Thread.isMainThread else {
return DispatchQueue.main.async(execute: completion)
}
completion()
}
Closure を定義して受け取った処理をメインスレッドで実行するようにしています。
拡張クラスは対象クラスと同じインターフェイス
今回の API データ取得は、以下のような Protocol に準拠した実装のため
protocol UserDetailUsecase: AnyObject {
func fetchList(userName: String, completion: @escaping (Result<[Repository]>) -> Void)
}
Java だと別クラスを用意して Implementsしているが、Swift は Extension 拡張で解決できる。(今回データ取得を Protocol を使って実装しているためで、Class 実装の場合は同様に継承させないといけない)
Extension で実現することで、データ取得処理毎に、拡張クラスを用意せずに、Extension を用意するだけなのでスマートですね。
これは、Retroactive Modeling というもので、DispatchMainQueueDecorator クラス自体には、UserDetailUsecase
の機能は定義されていないのだが、後から追加しています。
【iOSDC2019 補足資料】具体的なコードから始めよ ~今の問題を解決し、ジェネリックなコードを見出す~
これはクラス継承ではできないことです。 クラス継承の場合は型を定義する時に親クラスを決める必要があります。
必要としているどんな型でも 自分の Protocol を適合させることができ 元々の型の作成者が 思いもよらない新しい強力な方法で使うことができるようになります。
Protocol 化するメリット大きいですね!!すごく勉強になりました。
- 拡張クラスは対象クラスと同じインターフェイスを実現させる
クラスとしてではありませんが、実現していますね。
extension DispatchMainQueueDecorator: UserDetailUsecase where T == UserDetailUsecase {
func fetchList(userName: String, completion: @escaping (Result<[Repository]>) -> Void) {
decoratee.fetchList(userName: userName) { [weak self] result in
self?.dispatch {
completion(result)
}
}
}
}
拡張対象の SearchUserUsecase
Protocol に準拠させて、T 型を where 句で UserDetailUsecase
適合しています。
データ取得後の処理 completion(result)
を
self?.dispatch {
// ここには任意の処理が渡されるがメインスレッドで実行されることが保証されている
}
で囲むことで、メインスレッドで処理するようにしています。
Decorator 利用側
static func assembleModules(userName: String) -> UIViewController {
let vc = UserDetailViewController()
let router = UserDetailRouter(viewController: vc)
let userDetailInteractor = UserDetailInteractor()
- let presenter = UserDetailPresenter(view: vc, router: router, userDetailInteractor: userDetailInteractor, userName: userName)
+ let presenter = UserDetailPresenter(view: vc, router: router, userDetailInteractor: DispatchMainQueueDecorator(userDetailInteractor), userName: userName)
vc.presenter = presenter
return vc
}
DispatchMainQueueDecorator( )
でクラスを囲むだけでの変更。
UserDetailUsecase の Protocol に準拠するものを受け取るインターフェイスになっているため
extension DispatchMainQueueDecorator: UserDetailUsecase
のように extension でUserDetailUsecase
に準拠させているので変更する必要がありません。Protocol + Retroactive Modeling のおかげですね!
final class UserDetailPresenter {
private var userName: String
private weak var view: UserDetailView?
private let router: UserDetailWireframe
private let userDetailInteractor: UserDetailUsecase
init(view: UserDetailView, router: UserDetailWireframe, userDetailInteractor: UserDetailUsecase, userName: String) {
self.view = view
self.router = router
self.userDetailInteractor = userDetailInteractor
self.userName = userName
}
}
メインスレッド変換処理がなくなりスッキリ!
func viewDidLoad() {
userDetailInteractor.fetchList(userName: userName) { [weak self] result in
switch result {
case let .success(repositories):
+ self?.view?.reload(list: repositories)
case let .failure(error):
+ self?.view?.showError(title: error.localizedDescription, message: nil)
}
}