今更だけどSwiftの型消去の使い所が少し分かったので備忘録
背景
2016年のtry!Swiftで話題になった型消去
なかなか使い所が分かってなかったんだけど、利用したいシーンに出会ったので備忘録を記す
説明
前提
私は、開発する上で、よくHTTPクライアントのライブラリであるAPIKitを使っています。
APIKitの使い方を簡単に説明すると、Request
というプロトコルに準拠したクラスを書いて、APIKitのSession
クラスのsend
メソッドの引数に指定することで、指定したエンドポイントにアクセスして結果を返してくれます。
このAPIKitのRequest
プロトコルは、エンドポイントにアクセスして取得するレスポンスの型をassociatedtype
を使って、Request
プロトコルに準拠したクラスで指定できるようになっています。
public protocol Request { /// The response type associated with the request type. associatedtype Response //その他、HttpMethodやpathやパラメータなどのプロパティがある //.... }
下記は、あるAPIを叩いて成功した場合にStringの配列を返すクラスです。
public class HogeEndpoint: Request { typealias Response = [String] //... }
実際に使う場合は下記のような感じになります
let request = HogeEndpoint() Session(request) { result in switch result { case .success(let items): //Stringの配列を取得 case .failure(let error): //Error処理 } }
型消去を使って解決した課題
APIKitの説明を踏まえた上で、Request
プロトコルに準拠したクラスで、APIのエンドポイントは違うけれども成功した時に返ってくる値の型は同じであるパターンを想定してみます。
例えば下記のようなHogeEndpoint
とFugaEndpoint
クラスです。
class HogeEndpoint: Request { typealias Response = [String] var path: String = "/api/v1/hoge" //... } class FugaEndpoint: Request { typealias Response = [String] var path: String = "/api/v1/fuga" //... }
これは、接続するエンドポイントは違いますが、結果として得られる値の型は同じです。
そこでString型の配列をリストで表示する画面を作ることを想定してみましょう。
リスト表示のViewControllerにRequest
プロトコルのStored propertyを宣言してやれば、ViewControllerが汎用的に使えると思い下記のようなコードを書きました。
import UIKit import APIKit class StringListTableViewController: UITableViewController { var request: Request! private var items: [String]? { didSet { tableView.reloadData() } } override func viewDidLoad() { super.viewDidLoad() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier") fetchItem() } private func fetchItem() { Session.send(request) { [weak self] result in switch result { case .success(let items): self?.items = items case .failure(let error): print(error) } } } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items?.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) cell.textLabel?.text = items?[indexPath.row] return cell } }
これで色々な画面から、String配列のリスト表示画面であるStringListTableViewController
のStored propertyであるrequest
に値を渡すだけで汎用的に使えると思うかもしれません。
しかし、これではProtocol 'Request' can only be used as a generic constraint because it has Self or associated type requirements.
エラーになります。
Request
プロトコルのassociatedtype
の型が決まってないのでダメよってことですね。
当然といえば当然です。
ならばRequest
プロトコルにジェネリクスを使って下記のようにすればと思いますが残念ながらSwiftはプロトコルでのジェネリクスをサポートしていません。
public protocol Request<Result> { /// The response type associated with the request type. associatedtype Response //その他、HttpMethodやpathやパラメータなどのプロパティがある //.... } extension Request { typealias Response = Result }
Protocols do not allow generic parameters; use associated types instead
では、こんな時はどうすれば良いでしょうか?
結局、Request
プロトコルのassociatedtype
に引きづられて、同じようなViewを作るしかないのでしょうか?
いや待ってください。そうです。こんな時に型消去です。
型消去
(型消去自体の説明は沢山良記事があるので割愛します)
このエラーを回避してViewControllerを汎用的に使い回すために下記のような型消去クラスを作成します
struct TypeEraseEndpoint<ResponseType>: Request { typealias Response = ResponseType var path: String var method: HTTPMethod private let responseHandler: ((Any, HTTPURLResponse) throws -> ResponseType) init<R: Request>(request: R) where R.Response == ResponseType { self.path = request.path self.queryParameters = request.queryParameters self.method = request.method self.dataParser = request.dataParser self.responseHandler = request.response } func response(from object: Any, urlResponse: HTTPURLResponse) throws -> ResponseType { return try responseHandler(object, urlResponse) } }
このような型消去のクラスを使うことで、先ほどのStringListTableViewController
のStored propertyであるrequest
の型をTypeEraseEndpoint<[String]>!
に変更することで、ViewControllerを汎用的に使うことができました。
import UIKit import APIKit class StringListTableViewController: UITableViewController { var request: TypeEraseEndpoint<[String]>! private var items: [String]? { didSet { tableView.reloadData() } } override func viewDidLoad() { super.viewDidLoad() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier") fetchItem() } private func fetchItem() { Session.send(request) { [weak self] result in switch result { case .success(let items): self?.items = items case .failure(let error): print(error) } } } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items?.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) cell.textLabel?.text = items?[indexPath.row] return cell } }
この型消去のサンプルプロジェクトをQiitaのAPIを使って作ってみました。
もしよければ触ってみてください。
ただし、TypeEraseのジェネリクスにProtocol型を指定して柔軟にとかは出来ない・・
最後に
そもそもCleanArchitectureみたいなレイヤードアーキテクチャを意識して作れば、このような場面には出くわさないんですけどね。
ViewControllerにRequestプロトコル渡すような書き方するなよとか言わないで!!
とはいえ、ほんと今更の理解で凹む。。
普段からassociatedType
のProtocolを書いていないという設計力の低さを露呈しているような気がする。
iOS CleanArchitectureを使ってみた感想
CleanArchitectureを使ったサンプルアプリを作成したので、説明していきたいと思います
サンプルアプリ
まず、作成したアプリについてです
Qiitaのクライアントです
認証 | 投稿一覧 | 詳細 | ユーザー投稿一覧 | 詳細 |
---|---|---|---|---|
ソースコード
https://github.com/hachinobu/CleanQiitaClient
使い方
一応そのままビルドしてもビルドは通りますが認証していない状態なのでAPIコール制限とストックボタンなどは動かないです
認証したい場合は下記からアプリケーションの登録をしてください https://qiita.com/settings/applications/new
リダイレクト先のURLをサンプルソースコードでは固定にしてしまっているので clean-qiita-client://oauth で登録すると楽です Client IDとClient Secretを取得したらそれぞれ、サンプルソースコードの Secrets.swiftというファイルを開き、
public static let clientId: String = "****" public static let clientSecret: String = "****"
を書き換えればOKです
先に記載したリダイレクト先のURLについても、違う名前で登録したのであれば、
public static let redirectUrlScheme: String = "clean-qiita-client"
ここを修正すれば動きます
環境
Xcode8 + Swift3.0 cocoapods-1.1.0.rc.2以上
Clean Architectureとは
ドメイン駆動開発(DDD)やユースケース駆動開発(UCDD)を意識して、ビジネスロジックをUIやFrameworkから引き離し、それぞれのレイヤー毎に役割と責任を分離したArchitectureになります
概念などについては割愛しますが下記の記事を何ども読み込みました
まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて
各レイヤーと対象のサンプルコード
下記がCleanArchitectureのLayerの構造になっています
※ 今回は上記の絵に加え、PresentationLayerにRoutingという層を作っています
それでは各レイヤーについてサンプルコードを見ながら説明します
DataLayer
- 通信やデータ管理のロジックを担当するレイヤー
DataStore
- データを取得/更新する処理を担当
- 取得先がAPIならAPI用のDataStore,取得先がDBならDBから取得用のDataStoreができる
- あるデータを取得する際に取得先(APIやDB)が複数ある場合、RepositoryからFactoryクラスを用いてDataStoreを生成する
- APIの場合、Endpointごとにつくるイメージ
DataStoreサンプルコード
protocol ItemListDataStore { func fetchAllItemList(page: Int?, perPage: Int?, handler: @escaping (Result<GetAllItemListRequest.Response, SessionTaskError>) -> Void) } struct ItemListDataStoreNetworkImpl: ItemListDataStore { func fetchAllItemList(page: Int?, perPage: Int?, handler: @escaping (Result<GetAllItemListRequest.Response, SessionTaskError>) -> Void) { let request = GetAllItemListRequest(page: page, perPage: perPage) Session.send(request, callbackQueue: nil, handler: handler) } } struct ItemListDataStoreFactory { static func createItemListDataStore() -> ItemListDataStore { return ItemListDataStoreNetworkImpl() } }
ItemListDataStoreNetworkImpl
はQiitaの記事一覧をAPIから取得するDataStoreです
ItemListDataStoreFactory
はQiitaの記事一覧を取得するDataStoreを生成します
本サンプルではAPI経由でしかデータを取得しないようになっているので必ずItemListDataStoreNetworkImpl
を生成して返すようにしています
またNetwork部分はAPIKitに依存したサンプルコードになっています
例えば記事一覧をAPIからだけでなく、キャッシュしているDBからデータを取得したい場合などは、ItemListDataStoreFactory
の中で取得すべき条件に応じて分岐処理を書いて正しいデータの取得先となるDataStoreを生成します
ItemListDataStoreFactory
はItemListDataStore
を返すようにしています
ItemListDataStore
は記事一覧を取得するDataStoreのI/Fです
APIからデータを取得するDataStoreもDBからデータを取得するDataStoreも共にItemListDataStore
に準拠することでDataStoreを使うRepositoryは何の意識もすることがなくI/Fであるデータ取得のメソッドを叩くだけで良くなります
Entity
- DataStoreで扱うことができるデータの静的なモデル
- データ取得先から取得したデータのValueObject
- EntityはPresentationLayerでは使用しない(Viewのレンダリングなどに使われない)
Entityサンプルコード
import Foundation import ObjectMapper public struct ItemEntity { public var renderedBody: String? public var body: String? public var coediting: Bool? public var createdAt: String? public var group: GroupEntity? public var id: String? public var isPrivate: Bool? public var tagList: [TagEntity]? public var title: String? public var updatedAt: String? public var url: String? public var user: UserEntity? } extension ItemEntity: Mappable { public init?(map: Map) { } mutating public func mapping(map: Map) { renderedBody <- map["rendered_body"] body <- map["body"] coediting <- map["coediting"] createdAt <- map["created_at"] group <- map["group"] id <- map["id"] isPrivate <- map["private"] tagList <- map["tags"] title <- map["title"] updatedAt <- map["updated_at"] url <- map["url"] user <- map["user"] } }
APIのレスポンスのJSONをObjectMapperでマッピングしてValueObjectとなるEntityを生成しています
Repository
- DomainLayerのUseCaseにデータ取得のCRUD相当のI/Fを提供する
- 取得したいデータのDataStoreをFactoryクラスを用いて取得し、そのDataStoreに対してデータ取得/更新のリクエストを行う
- DataLayerとDomainLayerのI/Fを担う
今回DataLayerの1つとしてRepositoryを書いています
サンプルコード
import Foundation import APIKit import Result public protocol ItemListRepository { func fetchItemList(page: Int?, perPage: Int?, handler: @escaping (Result<GetAllItemListRequest.Response, SessionTaskError>) -> Void) } public struct ItemListRepositoryImpl: ItemListRepository { public static let shared: ItemListRepository = ItemListRepositoryImpl() public func fetchItemList(page: Int?, perPage: Int?, handler: @escaping (Result<GetAllItemListRequest.Response, SessionTaskError>) -> Void) { let dataStore = ItemListDataStoreFactory.createItemListDataStore() dataStore.fetchAllItemList(page: page, perPage: perPage, handler: handler) } }
Repositoryはデータ取得/更新のリクエストを出すDataStoreを生成時に注入される必要がないため生成コストを考慮してシングルトンで実装しています
UseCaseからfetchItemList
メソッドが叩かれたら、取得したいデータのDataStoreを生成するFactoryからDataStoreを取得し、そのDataStoreにデータ取得/更新のリクエストを実行します
DomainLayer
Model
- PresentationLayerで使用する主にViewのレンダリングに最適されたもの
サンプルコード
import Foundation public struct ListItemModel { public let id: String public let userName: String public let title: String public let tagList: [String] public let profileImageURL: String }
主にModelの値をViewに表示します
Translator
- UseCaseで取得したEntityをPresentationLayerで使用するModelへ変換する
import Foundation protocol Translator { associatedtype Input associatedtype Output func translate(_: Input) -> Output }
import Foundation import DataLayer struct ListItemModelsTranslator: Translator { func translate(_ entities: [ItemEntity]) -> [ListItemModel] { return entities.map { ListItemModelTranslator().translate($0) } } } struct ListItemModelTranslator: Translator { func translate(_ entity: ItemEntity) -> ListItemModel { let id = entity.id ?? "" let userName = entity.user?.id ?? "" let title = entity.title ?? "" let tagList = entity.tagList?.flatMap { $0.name } ?? [] let profileImageURL = entity.user?.profileImageUrl ?? "" return ListItemModel(id: id, userName: userName, title: title, tagList: tagList, profileImageURL: profileImageURL) } }
ここではItemEntity
からListItemModel
に変換しています
ここでの変換処理は主にOptionalを外したりViewに最適化の処理をしています
最適化といえど金額Int(1000)からFormattingされた金額String(¥1,000)などはしません この処理はPresentationLayerのView(ViewModel)で行うべきです
UseCase
- ユースケースに必要なロジック処理を記述する
- UIには直接関与しない(View,ViewControllerから直接参照されない)
- PresentationLayerで使用するModelを生成するためにRepositoryを複数持つ可能性がある(複数APIを叩かなければ生成できないModelなど
- Repositoryを叩いてEntityを取得してTranslatorでModelへ変換させる
- 必要なModelを生成するための材料となるEntityが全て揃うまでの待ち合わせなどもここで行う
サンプルコード
public protocol ItemListUseCase { func fetchItemList(page: Int?, perPage: Int?, handler: @escaping (Result<[ListItemModel], SessionTaskError>) -> Void) } public struct AllItemListUseCaseImpl: ItemListUseCase { let repository: ItemListRepository public init(repository: ItemListRepository) { self.repository = repository } public func fetchItemList(page: Int?, perPage: Int?, handler: @escaping (Result<[ListItemModel], SessionTaskError>) -> Void) { repository.fetchItemList(page: page, perPage: perPage) { result in let r = generateItemModelsResultFromItemEntitiesResult(result: result) handler(r) } } } fileprivate func generateItemModelsResultFromItemEntitiesResult(result: Result<[ItemEntity], SessionTaskError>) -> Result<[ListItemModel], SessionTaskError> { guard let itemEntities = result.value else { return Result.failure(result.error!) } let listItemModels = ListItemModelsTranslator().translate(itemEntities) return Result.success(listItemModels) }
取得すべきデータのI/Fを提供しているRepositoryが注入されているので、そのRepositoryのI/Fを叩くとDataLayerで取得したEntityを取得できるので、そのEntityをTranslatorにかけ最終成果物となるModelを生成します
UseCaseを使うPresenterは、画面を生成するのに何個APIを叩く必要があるといったことは知る必要はなくViewにレンダリングさせるModelを要求するだけで、UseCase内で全てRepositoryなどを管理して通信の待ち合わせなどをしModelを生成します
PresentationLayer
- UIの表示やイベントのハンドリングを行います
- ビジネスロジック処理はしません
Presenter
- Viewからイベントを受け取り、必要があればイベントに応じたUseCaseを実行する
- イベントに応じてViewにレンダリングすべきUIを指定する
- UseCaseから受け取ったデータをViewへ渡す
- UseCaseから受け取ったデータを管理する
サンプルコード
import Foundation import DomainLayer public protocol ItemListPresenter { weak var view: ItemListPresenterView? { get set } func setupUI() func refreshData() func fetchMorePageItem() func reachedBottom(index: Int, isAnimation: Bool) func selectedItem(index: Int) } public protocol ItemListPresenterView: class { func showErrorAlert(message: String) func setupNavigation(title: String) func setupRefreshControl() func segueItemDetailScreen(itemId: String) func reloadView(itemListSummaryProtocol: ItemListSummaryProtocol) func startIndicator() func stopIndicator() }
ItemListPresenterプロトコルは一覧画面のViewからイベントなどを受け取って処理するPresenterのI/Fを提供しています
setupUI
メソッドではUIの初期化
refreshData
メソッドでは一覧の表示に必要なデータの取得
などです
Viewがイベントやライフサイクルに応じてPresenterのI/Fを叩きます
ItemListPresenterViewはItemListPresenterの処理が完了した時に、Viewがどのような振る舞いをすれば良いのかのI/Fを提供しています ViewがItemListPresenterViewに準拠していればPresenterからレンダリングすべきデータを受け取ったり、Presenterからの要求を受け取ることができます
ItemListPresenter
の実装です
import Foundation import DomainLayer import Kingfisher import Networking import APIKit import Result public class AllItemListPresenterImpl: ItemListPresenter { weak public var view: ItemListPresenterView? let useCase: ItemListUseCase private var currentPage: Int = 1 let perPage = 20 private var listItemModels: [ListItemModel] = [] { didSet { reloadView(listItemModels: listItemModels) } } private var isFinishMoreLoad: Bool = false public init(useCase: ItemListUseCase) { self.useCase = useCase } public func setupUI() { view?.setupNavigation(title: "全ての投稿") view?.setupRefreshControl() } public func refreshData() { ImageCache.default.clearMemoryCache() let firstPage = 1 useCase.fetchItemList(page: firstPage, perPage: perPage) { result in switch result { case .success(let listItemModels): self.currentPage = firstPage self.listItemModels = listItemModels case .failure(.responseError(let qiitaError as QiitaError)): self.view?.showErrorAlert(message: qiitaError.message) case .failure(let error): self.view?.showErrorAlert(message: error.localizedDescription) } } } public func fetchMorePageItem() { view?.startIndicator() let nextPage = currentPage + 1 useCase.fetchItemList(page: nextPage, perPage: perPage) { result in self.view?.stopIndicator() guard let listItemModels = result.value else { return } self.currentPage = nextPage if listItemModels.count == 0 { self.isFinishMoreLoad = true } self.listItemModels = self.mergeItemList(currentListItemModels: self.listItemModels, fetchListItemModels: listItemModels) } } public func reachedBottom(index: Int, isAnimation: Bool) { let bottomIndex = listItemModels.count - 1 guard bottomIndex == index && !isAnimation && !isFinishMoreLoad else { return } fetchMorePageItem() } public func selectedItem(index: Int) { guard listItemModels.count > index else { return } let item = listItemModels[index] view?.segueItemDetailScreen(itemId: item.id) } private func mergeItemList(currentListItemModels: [ListItemModel], fetchListItemModels: [ListItemModel]) -> [ListItemModel] { return fetchListItemModels.reduce(currentListItemModels) { (currentList, fetchItem) in return currentList.contains { $0.id == fetchItem.id } ? currentList : currentList + [fetchItem] } } private func reloadView(listItemModels: [ListItemModel]) { let summaryVM = ItemListSummaryVM(itemList: listItemModels) view?.reloadView(itemListSummaryProtocol: summaryVM) } }
Viewのイベントに対応したメソッドを実装しています 一覧画面を表示するために必要なデータの取得をUseCaseに命令したり 表示するUIの初期設定やRefreshControlの有無などを判断してViewに命令を出しています
View(ViewController)
- Viewのライフサイクル・レンダリング・ユーザーのタッチイベントなどのイベントをPresenterに通知する
- Presenterから受けたModelのデータやステータスによりViewの表示を切り替える
サンプルコード
import UIKit import DomainLayer import Utility fileprivate extension Selector { static let refreshAction = #selector(ItemListViewController.refreshData) } public class ItemListViewController: UITableViewController { @IBOutlet weak var indicatorCircleView: IndicatorCircleView! private var presenter: ItemListPresenter! { didSet { presenter.view = self } } fileprivate var routing: ItemListRouting! fileprivate var itemListSummaryVM: ItemListSummaryProtocol! { didSet { tableView.reloadData() } } public func injection(presenter: ItemListPresenter, routing: ItemListRouting) { self.presenter = presenter self.routing = routing } override public func viewDidLoad() { super.viewDidLoad() presenter.setupUI() presenter.refreshData() } override public func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } func refreshData() { presenter.refreshData() } // MARK: - Table view data source override public func numberOfSections(in tableView: UITableView) -> Int { let numberOfSection = itemListSummaryVM == nil ? 0 : 1 return numberOfSection } override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return itemListSummaryVM.fetchItemCount() } override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as ListItemCell let displayItemVM = itemListSummaryVM.fetchListItemDisplayProtocol(index: indexPath.row) cell.setupContents(displayVM: displayItemVM) return cell } override public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { presenter.reachedBottom(index: indexPath.row, isAnimation: indicatorCircleView.isAnimating()) } // MARK: - Table view delegate override public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableViewAutomaticDimension } override public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { return UITableViewAutomaticDimension } override public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { presenter.selectedItem(index: indexPath.row) } } extension ItemListViewController: ItemListPresenterView { public func showErrorAlert(message: String) { refreshControl?.endRefreshing() routing.presentErrorAlert(message: message) } public func setupNavigation(title: String) { self.title = title } public func setupRefreshControl() { guard refreshControl == nil else { return } refreshControl = UIRefreshControl() refreshControl?.tintColor = .qiitaMainColor refreshControl?.addTarget(self, action: .refreshAction, for: .valueChanged) } public func segueItemDetailScreen(itemId: String) { routing.segueItem(id: itemId) } public func reloadView(itemListSummaryProtocol: ItemListSummaryProtocol) { refreshControl?.endRefreshing() itemListSummaryVM = itemListSummaryProtocol } public func startIndicator() { indicatorCircleView.startAnimation() } public func stopIndicator() { indicatorCircleView.stopAnimation() } }
イベントに応じてPresenterのI/Fを叩いているのが分かります またItemListPresenterViewに準拠することで、Presenterからの指示のもとViewが一覧を表示したりエラーダイアログを出したり命令を受けます Viewは渡されたデータをレンダリングしたり、Presenterから指示された通りの処理をするだけです View本来の役割です
各レイヤーはDIできるようになっており、ViewControllerのInjectメソッドで依存性が注入されます
Routing
- 画面遷移をつかさどる
- DIもここでおこなう
サンプルコード
public protocol AuthScreenRouting: Routing { func segueAllItemListScreen() }
import Foundation import DataLayer import DomainLayer import PresentationLayer public struct AuthScreenRoutingImpl: AuthScreenRouting { weak public var viewController: UIViewController? public init() { } public func segueAllItemListScreen() { let repository = ItemListRepositoryImpl.shared let useCase = AllItemListUseCaseImpl(repository: repository) let presenter = AllItemListPresenterImpl(useCase: useCase) let navigationController = UIStoryboard(name: "ItemListScreen", bundle: Bundle(for: ItemListViewController.self)).instantiateInitialViewController() as! UINavigationController let viewController = navigationController.topViewController as! ItemListViewController let routing = AllItemListRoutingImpl() routing.viewController = viewController viewController.injection(presenter: presenter, routing: routing) UIApplication.shared.delegate?.window??.rootViewController = navigationController let appDelegate = UIApplication.shared.delegate appDelegate?.window??.rootViewController = navigationController appDelegate?.window??.makeKeyAndVisible() } }
認証画面後に一覧画面が表示されるので認証画面から一覧画面に遷移する際に呼ばれます
ここでは一覧画面を構成する上で必要なRepository,UseCase,Presenter,Routingを一覧画面のViewControllerに注入しています DIができる設計になっているので簡単にレイヤー間を差し替えることが可能です
これだけレイヤーが分かれていて、かつDIしやすい設計になっているメリットの一例として [投稿一覧画面]→[詳細画面]に遷移した際に、ユーザー名のリンクをタップすると[ユーザー投稿一覧画面]に遷移します 次に[ユーザー投稿一覧画面]から記事を選択すると[詳細画面]に遷移しますが、ここでまたユーザー名をタップした場合、Pushで[ユーザー投稿一覧画面]に遷移してしまうのは変ですよね? [ユーザー投稿一覧画面]から遷移してきているだからpopするだけで良い
こんな時、注入するRoutingを変えてあげるだけで他のレイヤーは何一つ変えることなく上記の画面遷移を実現できるのです
サンプルコードでいうと
下記は[投稿一覧]→[詳細]に遷移した場合のユーザー名をタップした挙動を記述しているRoutingです
import Foundation import DataLayer import DomainLayer import PresentationLayer class ItemRoutingImpl: ItemRouting { weak var viewController: UIViewController? { didSet { viewController?.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) } } func segueItemList(userId: String) { let repository = UserItemListRepositoryImpl.shared let useCase = UserItemListUseCaseImpl(repository: repository, userId: userId) let presenter = UserItemListPresenterImpl(useCase: useCase) let navigationController = UIStoryboard(name: "ItemListScreen", bundle: Bundle(for: ItemListViewController.self)).instantiateInitialViewController() as! UINavigationController let vc = navigationController.topViewController as! ItemListViewController let routing = UserItemListRoutingImpl() routing.viewController = vc vc.injection(presenter: presenter, routing: routing) viewController?.navigationController?.pushViewController(vc, animated: true) } }
ここではpushでタップしたユーザーの一覧画面に遷移するよう指示しています
次に、[ユーザー投稿一覧]→[詳細]に遷移してからユーザーのリンクをタップした際の挙動を記載したRoutingです
import Foundation import DataLayer import DomainLayer import PresentationLayer class UserItemRoutingImpl: ItemRoutingImpl { override func segueItemList(userId: String) { let _ = viewController?.navigationController?.popViewController(animated: true) } }
ここでは詳細画面でユーザー名をタップした際に1つ前の画面に戻るようにしています
一覧画面から記事を選択した際の詳細画面のViewControllerにRepository,UseCase,Presenter,RoutingをDIできるようになっているので上記のようなことが簡単に実現可能なのです
Clean Architectureを使ってみて
メリット
- 各レイヤーの責務がしっかり分かれるのでどこに何を書くかとか迷わない
- レイヤーをこれだけ細かく切るのでDIできる設計にすれば変更要求に強いプログラムがかける
- スケールしやすい
- テスト書きやすい
デメリット
- 学習コスト
- コード量が多くなる
- とりあえずモノを世に出さないとというスピード最重視の0->1フェーズスタートアップでは使わないかも
個人的には使ってみて凄く良かった やはりViewを本来のレンダリングだけすれば良いといった役割にできること、変更要求の強さに魅力を感じています アプリはViewが一番変わると思うのでCleanArchitectureを導入しないにしても、PresentationLayerの概念だけは意識しながら分けてあげると良いなと思います
謝辞
今回の説明はほぼ
まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて
から引用させていただいてます
@koutalou 本当にありがとうございました
CleanArchitectureのサンプルコードを書く上での基盤や構造などを教えてくれたyukiasai ありがとうございました!
UITableViewCellが無い領域のUITableViewのSeparatorを消す方法
背景
TableViewCellが表示されている領域にはSeparatorあって良いんだけど、Cellが1個しかなくて後はUITableViewの領域で、その領域ではSeparatorを表示したくなかった
解決
TableViewのTableFooterViewに何か突っ込めばUITableView領域にSeparatorは出ない
tableView.tableFooterView = UIView(frame: CGRectZero)
SwiftTaskであるAPI処理の結果をもとにSuccessでTask.allを使う方法
背景
ある通信処理の成功結果をもとに複数通信をネストせずに書きたかった SwiftTaskを使うと簡単にできる
コード
private func generateTaskA() -> Task<Float, [String], NSError?> { return Task { (fulfill, reject) in //通信処理 成功時にはStringの配列が得られる fulfill(results) } } private func generateTaskB(successA: String) -> Task<Float, Int, NSError?> { return Task<Float, DestinationPlaceProtocol, NSError?> { progress, fulfill, reject, configure in //通信処理 成功時にはInt型の値が得られる fulfill(result) } } func fetchData() { generateTaskA().success { [unowned self] results -> Task<(completedCount: Int, totalCount: Int), [Int], NSError?> in //TaskAの成功時 results(Stringの配列)をもとにTaskBを複数作成 let taskListB = results.map(self.generateTaskB) return Task.all(taskListB) }.success { numberResults in //TaskBの配列のTaskが全て成功した場合にくる //TaskBの成功オブジェクトの配列なのでnumberResultsは[Int] } }
1つ目のsuccessの返り値であるTaskはTaskBの配列の結果として何が得られるかを指定する(successの中で処理するTaskの結果)
Task.allの場合は
public class func all(tasks: [Task]) -> Task<BulkProgress, [Value], Error>
返り値が上記のようになるので合わせる (BulkProgressは(completedCount: Int, totalCount: Int)のエイリアス)
Swiftのインスタンスから型メソッド(クラスメソッド)を呼び出す方法
背景
あるクラスのインスタンスから、そのクラスの型メソッド(クラスメソッド)を呼び出したいときにObjective-Cでは下記のようにやっていたけれどSwiftだとどう書くのか知りたかった
[[instance class] classMethod];
結論
dynamicTypeを使うことでインスタンスから型メソッド(クラスメソッド)へのアクセスが可能になる
class SampleClass() { //型メソッド(クラスメソッド) static func sampleClassMethod() { print("sampleClassMethod") } } //普通の型メソッド(クラスメソッド)の呼び出し方 SampleClass.sampleClassMethod() //sampleClassMethod //インスタンスからクラスメソッドへのアクセス let sampleClass = SampleClass() sampleClass.dynamicType.sampleClassMethod() //sampleClassMethod
dynamicTypeとはなんぞや?
dynamicTypeを使うとインスタンスのクラス自身(サブクラス化されていればサブクラス)が参照できるようです
let sampleClass = SampleClass() print(sampleClass.dynamicType) //SampleClass if sampleClass.dynamicType == SampleClass.self { print("true") //true }
tableViewCell上にUISliderを載せてアニメーションしたらおかしくなる件
事象
カスタムのUITableViewCell上にUISliderをaddしてcellforRowで読み込まれると同時にSliderをアニメーションしてみたのだけれど、minimumTrackTintColorとmaximumTrackTintColor割合がおかしく、明らかに変なアニメーションをしだした。
解決方法
Sliderのアニメーションをdispatch_afterで遅らせてやるとうまくいった。
ちなみに[UIView animateKeyframesWithDuration...のdelay:をセットしてもダメだった。
下記サンプルではtag10にUISliderが紐付けられているものとする。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ UISlider *slider = (UISlider *)[cell viewWithTag:10]; slider.value = 0; [UIView animateKeyframesWithDuration:1 delay:0 options:UIViewKeyframeAnimationOptionAllowUserInteraction animations:^{ [slider setValue:0.8 animated:YES]; } completion:^(BOOL finished) { }]; }); return cell; }
うまくいかない時は遅延処理させると解決するのは結構あるけど、 delayじゃなくdispatch_after使わないとダメだったとは。。
かなりの時間を使ってしまったので久しぶりにメモ。
Android Studio0.8.14にしたらエラーしまくった
背景
Android Studioのverを0.8.14にしたらエラーになったので解決方法をメモ
解決方法
まず、アップデートしているとAndroid Studio配下にsdkフォルダを配置するなと怒られるので、sdkの場所をmvコマンドで適当な場所に移す。
sdkを移した後にリトライすれば0.8.14に上がる。
Android Studioを起動した際にsdkがないと言われるので、sdkを移動したパスを設定する。
その後に、新規プロジェクト作成したら今度はAndroid APIの最新である21を使うにはjdk1.7を使えというエラー。
OracleからJava SE Development Kit 7u71をダウンロードしてjdkをjdk1.7.0_71.jdk/Contents/Homeを使うように設定してようやく動いた。
Android Studioはまだbeta版だからいろいろと仕様が変わるのですね。
ありえん。。
追記 新規でAndroid Studio0.8.14をインストールする場合はここが大変参考になります。