漸進的にViewControllerの肥大化を防ぐ

1.2K Views

December 01, 19

スライド概要

俺コン 2018で共有した、「漸進的にViewControllerの肥大化を防ぐ」の内容です。

profile-image

2023年10月からSpeaker Deckに移行しました。最新情報はこちらをご覧ください。 https://speakerdeck.com/lycorptech_jp

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

(ダウンロード不可)

関連スライド

各ページのテキスト
1.

漸進的にViewController の肥大化を防ぐ Kazuhiro HAYASHI 俺コン Day2

2.

自己紹介 ・ 林和弘 ・ Twitter: kazuhiro494949 ・ github, qiita: kazuhiro4949 ・ Yahoo! Japan Corp. ・ iOSアプリエンジニア ・ iOSアプリ黒帯

3.

アジェンダ ・ クラス設計の議論で陥りがちな罠を自分なりに説明し、解 消の具体的なアプローチを検討

4.

しっかりとクラス設計をすることの本質 ・ システム的なテストを実現してエンバグを防ぐ ・ パーツの入れ替えによって状況の変化へ柔軟に対応

5.

そのためにやるべきこと ・ 依存性の注入 ・ 責務の分離 ・ インターフェースによるオブジェクト間のコミュニケーション

6.

極論 ・ 様々な議論は概ねこの辺の話を応用したものはない かと思う ・ 原理原則に従い目的を達成できていれば、レイヤ構成やク ラス設計はぶっちゃけなんでもよい

7.

例を元に漸進的に設計を行う

8.

ここでやらないこと ・ 特定のGUIアーキテクチャ(もしくはその亜種)に従わない ・ 今回はスコープ外なので、特定のライブラリは導入しない (RxSwift, Swinject etc.)

9.

ここでやること ・ 個別の目的を達成するために、パターンを参考にしつつ設 計の背景にある原理原則を取り入れていく ・ プロダクトのフェーズに合わせて自分たちの設計を育てて いく

10.

こんなプロダクトが既ににあるとする Timeline Swift Dev Feed Swift Top 10 Articles in September 2017 https://t.co/sHDXojQCDE #SwiftLang #iOSDev #iOSProgramming #Xcode #iOS 熊谷 友宏 @ 技術書典3 (え.09) 本日「俺コン」で『Protocol-Oriented Integers に想うジェネリックプログラミングの 未来』というテーマで話します。自分が感じる 些細なことをみんなと共有したくて。よろしく お願い致します。 #orecon_ios https://t.co/ z6w2kgTWNQ いたのくまんぼう・Unity入門書発売中 RT @newssamurai: 【アンケート】iOS11が配 信され、AppStoreが新しくなりました。皆様の アプリのダウンロード数に影響はありました か?お手数をおかけいたしますが、ご回答をい ただけると幸いです。よろしくお願いいたしま す。 ※メディア様の記事にアンケート結果を... はまあ@C93 Audi×美女写真集 RT @AnkRouge: 【Press】 \pick up blog/ 10月6日発売の ・ TwitterのAPIを叩いて、結果を CollectionViewで表示する

11.

UIView ↓ UIViewController ↑ Service ↑ APIClient ↑ OAuth Library

12.

追加要件1 Timeline Swift Dev Feed Swift Top 10 Articles in September 2017 https://t.co/sHDXojQCDE #SwiftLang #iOSDev #iOSProgramming #Xcode #iOS 熊谷 友宏 @ 技術書典3 (え.09) 本日「俺コン」で『Protocol-Oriented Integers に想うジェネリックプログラミングの 未来』というテーマで話します。自分が感じる 些細なことをみんなと共有したくて。よろしく お願い致します。 #orecon_ios https://t.co/ z6w2kgTWNQ いたのくまんぼう・Unity入門書発売中 RT @newssamurai: 【アンケート】iOS11が配 信され、AppStoreが新しくなりました。皆様の アプリのダウンロード数に影響はありました か?お手数をおかけいたしますが、ご回答をい ただけると幸いです。よろしくお願いいたしま す。 ※メディア様の記事にアンケート結果を... はまあ@C93 Audi×美女写真集 RT @AnkRouge: 【Press】 \pick up blog/ 10月6日発売の オフライン DB 通信 オンライン ・ DBと通信を切り替えられるよう にしたい

13.

UIView UIViewController Service ↑ APIClient ↑ OAuth Library ・ ServiceとAPIClientの間に Adapterをインターフェースとして 提供する ・ 環境や条件に合わせてDIを行う

14.
[beta]
class AnyRepository<S, F: Error>: Repository {
typealias Failure = F
typealias Success = S
let _find: (String, @escaping (Result<Success, Failure>) -> Void) -> Void
init<T: Repository>(_ repository: T) where T.Success == S, T.Failure == F {
self._find = repository.fetch
}
func fetch(userId: String, completeHandler: @escaping (Result<S, F>) -> Void) {
_find(userId, completeHandler)
}
}
protocol Repository {
associatedtype Failure: Error
associatedtype Success
func fetch(userId: String, completeHandler: @escaping (Result<Success, Failure>) -> Void)
}
・ ServiceとAPIClientの間に
Adapterをインターフェースとして
提供する
・ 環境や条件に合わせてDIを行う
15.
[beta]
class AnyRepository<S, F: Error>: Repository {
typealias Failure = F
typealias Success = S
let _find: (String, @escaping (Result<Success, Failure>) -> Void) -> Void
init<T: Repository>(_ repository: T) where T.Success == S, T.Failure == F {
self._find = repository.fetch
}
func fetch(userId: String, completeHandler: @escaping (Result<S, F>) -> Void) {
_find(userId, completeHandler)
}
}
protocol Repository {
associatedtype Failure: Error
associatedtype Success
func fetch(userId: String, completeHandler: @escaping (Result<Success, Failure>) -> Void)
}
・ ServiceとAPIClientの間に
Adapterをインターフェースとして
提供する
・ 環境や条件に合わせてDIを行う
16.
[beta]
class AnyRepository<S, F: Error>: Repository {
typealias Failure = F
typealias Success = S
let _find: (String, @escaping (Result<Success, Failure>) -> Void) -> Void
init<T: Repository>(_ repository: T) where T.Success == S, T.Failure == F {
self._find = repository.fetch
}
func fetch(userId: String, completeHandler: @escaping (Result<S, F>) -> Void) {
_find(userId, completeHandler)
}
}
protocol Repository {
associatedtype Failure: Error
associatedtype Success
func fetch(userId: String, completeHandler: @escaping (Result<Success, Failure>) -> Void)
}
・ ServiceとAPIClientの間に
Adapterをインターフェースとして
提供する
・ 環境や条件に合わせてDIを行う
17.

UIView ↓ UIViewController ↑ Service ↑ Repository ↑ APIClient ↑ OAuth Library DB

18.

追加要件2 Timeline Swift Dev Feed Swift Top 10 Articles in September 2017 https://t.co/sHDXojQCDE #SwiftLang #iOSDev #iOSProgramming #Xcode #iOS 熊谷 友宏 @ 技術書典3 (え.09) 本日「俺コン」で『Protocol-Oriented Integers に想うジェネリックプログラミングの 未来』というテーマで話します。自分が感じる 些細なことをみんなと共有したくて。よろしく お願い致します。 #orecon_ios https://t.co/ z6w2kgTWNQ いたのくまんぼう・Unity入門書発売中 RT @newssamurai: 【アンケート】iOS11が配 信され、AppStoreが新しくなりました。皆様の アプリのダウンロード数に影響はありました か?お手数をおかけいたしますが、ご回答をい ただけると幸いです。よろしくお願いいたしま す。 ※メディア様の記事にアンケート結果を... はまあ@C93 Audi×美女写真集 RT @AnkRouge: 【Press】 \pick up blog/ 10月6日発売の iPhone iPad ・ iPad用のデザインに対応してほ しい。iPad限定でLandscapeで も見れるようにしてほしい

19.

UIView UIViewController Service Repository APIClient DB Library ・ VC内で散らばった環境毎の分岐 をデザインオブジェクトへ切り出 す ・ 環境毎の設定を行ってDIする ・ 値を返すだけなのでインターフェー スは挟まないという判断

20.

class TimelineViewController: UICollectionViewController { // ... var layout = TimelineLayout.iPhone // ... override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) switch traitCollection.userInterfaceIdiom { case .phone, .tv, .unspecified, .carPlay: layout = .iPhone case .pad: layout = .iPad } } } struct TimelineLayout { let cellForLine: Int let needsAlign: Bool static var iPad: TimelineLayout { return TimelineLayout(cellForLine: 2, needsAlign: true) } static var iPhone: TimelineLayout { return TimelineLayout(cellForLine: 1, needsAlign: false) } } ・ VC内で散らばった環境毎の分岐 をデザインオブジェクトへ切り出 す ・ 環境毎の設定を行ってDIする ・ 値を返すだけなのでインターフェー スは挟まないという判断

21.

class TimelineViewController: UICollectionViewController { // ... var layout = TimelineLayout.iPhone // ... override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) switch traitCollection.userInterfaceIdiom { case .phone, .tv, .unspecified, .carPlay: layout = .iPhone case .pad: layout = .iPad } } } struct TimelineLayout { let cellForLine: Int let needsAlign: Bool static var iPad: TimelineLayout { return TimelineLayout(cellForLine: 2, needsAlign: true) } static var iPhone: TimelineLayout { return TimelineLayout(cellForLine: 1, needsAlign: false) } } ・ VC内で散らばった環境毎の分岐 をデザインオブジェクトへ切り出 す ・ 環境毎の設定を行ってDIする ・ 値を返すだけなのでインターフェー スは挟まないという判断

22.

class TimelineViewController: UICollectionViewController { // ... var layout = TimelineLayout.iPhone // ... override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) switch traitCollection.userInterfaceIdiom { case .phone, .tv, .unspecified, .carPlay: layout = .iPhone case .pad: layout = .iPad } } } struct TimelineLayout { let cellForLine: Int let needsAlign: Bool static var iPad: TimelineLayout { return TimelineLayout(cellForLine: 2, needsAlign: true) } static var iPhone: TimelineLayout { return TimelineLayout(cellForLine: 1, needsAlign: false) } } ・ VC内で散らばった環境毎の分岐 をデザインオブジェクトへ切り出 す ・ 環境毎の設定を行ってDIする ・ 値を返すだけなのでインターフェー スは挟まないという判断

23.

UIView ↓ UIViewController ↑ Service ↑ Repository ↑ APIClient ↑ OAuth Library DB Design Object

24.

追加要件3 Timeline Swift Dev Feed Swift Top 10 Articles in September 2017 https://t.co/sHDXojQCDE #SwiftLang #iOSDev #iOSProgramming #Xcode #iOS 熊谷 友宏 @ 技術書典3 (え.09) 本日「俺コン」で『Protocol-Oriented Integers に想うジェネリックプログラミングの 未来』というテーマで話します。自分が感じる 些細なことをみんなと共有したくて。よろしく お願い致します。 #orecon_ios https://t.co/ z6w2kgTWNQ いたのくまんぼう・Unity入門書発売中 RT @newssamurai: 【アンケート】iOS11が配 信され、AppStoreが新しくなりました。皆様の アプリのダウンロード数に影響はありました か?お手数をおかけいたしますが、ご回答をい ただけると幸いです。よろしくお願いいたしま す。 ※メディア様の記事にアンケート結果を... はまあ@C93 Audi×美女写真集 RT @AnkRouge: 【Press】 \pick up blog/ 10月6日発売の プロダクトを 展開 watchOS tvOS App Extension ・ App ExtensionやtvOS, watchOS を作りたい。その際にOAuthを 使った通信クラスをフレームワー ク化したい

25.

UIView UIViewController Service Repository APIClient DB OAuth Library Design Object ・ 依存しているライブラリが従うべ き定義をインターフェース化する ・ 通信部分だけFramework化でき る ・ 利用する際に依存をDIして、 Frameworkを利用する

26.

protocol LoginService { func refresh(completeHandler: (Error) -> Void) var accessToken: String { get } var isLogin: Bool { get } var isAccessTokenExpired: Bool { get } } class APIClientManager { fileprivate var oauthService: LoginService fileprivate var requestQueue = OperationQueue() init(oauthService: LoginService) { self.oauthService = oauthService } // ... // ... func refresh() { requestQueue.isSuspended = true if oauthService.isAccessTokenExpired { oauthService.refresh { [weak self] (error) in self?.requestQueue.isSuspended = false } } else { } } } ・ 依存しているライブラリが従うべ き定義をインターフェース化する ・ 通信部分だけFramework化でき る ・ 利用する際に依存をDIして、 Frameworkを利用する

27.

protocol LoginService { func refresh(completeHandler: (Error) -> Void) var accessToken: String { get } var isLogin: Bool { get } var isAccessTokenExpired: Bool { get } } class APIClientManager { fileprivate var oauthService: LoginService fileprivate var requestQueue = OperationQueue() init(oauthService: LoginService) { self.oauthService = oauthService } // ... // ... func refresh() { requestQueue.isSuspended = true if oauthService.isAccessTokenExpired { oauthService.refresh { [weak self] (error) in self?.requestQueue.isSuspended = false } } else { } } } ・ 依存しているライブラリが従うべ き定義をインターフェース化する ・ 通信部分だけFramework化でき る ・ 利用する際に依存をDIして、 Frameworkを利用する

28.

protocol LoginService { func refresh(completeHandler: (Error) -> Void) var accessToken: String { get } var isLogin: Bool { get } var isAccessTokenExpired: Bool { get } } class APIClientManager { fileprivate var oauthService: LoginService fileprivate var requestQueue = OperationQueue() init(oauthService: LoginService) { self.oauthService = oauthService } // ... // ... func refresh() { requestQueue.isSuspended = true if oauthService.isAccessTokenExpired { oauthService.refresh { [weak self] (error) in self?.requestQueue.isSuspended = false } } else { } } } ・ 依存しているライブラリが従うべ き定義をインターフェース化する ・ 通信部分だけFramework化でき る ・ 利用する際に依存をDIして、 Frameworkを利用する

29.

UIView UIViewController Service Repository APIClient DB OAuth Library APIClient Adapter Design Object

30.
[beta]
ツイートの中から特定のみ取っ
てきて別で表示させる
追加要件4
タイムライン
お気に入りツイート
にわタコ
僕は雪駄で出勤してます(˘ω˘)/ "スニーカー通
勤推奨「ビジカジ」(フジテレビ系
(FNN)) - Yahoo!ニュース" (105 users)
https://t.co/qKe9s10mYk
FUJI Goro
Google CrowlerにRubyやPostgreSQLの最新
バージョンを教える方法はないものか。ぐぐっ
て古いバージョンがでると、同じドキュメント
の最新版を探す旅が始まる...
NatashaTheNomad
Another amazing vegan cafe in #ChiangMai!
🍌, 🥜, &amp; goji berry energy balls 😍
#Thailand... https://t.co/QtK7C5yrB2 https://t.
co/d7mLJX8Eqr
Promote your apps with AdMob
www.admob.com
ボケて(人気)
父にキャバ嬢の名刺をちらつかせると/焼
ますか。とご機嫌をとってきました。 https://
t.co/OQoDPXGY7T
Timeline中に広告を差し込
む
・ 複数の通信を行ってそれらを組み
合わせた表示をしたい。表示方
法も、特定の情報をセグメントで
分離したい
31.

UIView UIViewController Service Repository APIClient DB Adapter Design Object ・ Presenterを介させる ・ 表示にそのまま対応するデータ構 造を構築して、構築するロジック だけテストできるようにクラスと して切り出す

32.
[beta]
enum TimelineElement {
case tweet(tweet: Tweet)
case ad(ad: Ad)
}
protocol TimelinePresenterAdapter: class {
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void)
}
class TimelinePresenter<T, S>: TimelinePresenterAdapter {
fileprivate let timelineFetcher: AnyFetcher<Tweet, TimelineError> // Service用のAdapterも用意する
fileprivate let adFetcher: AnyFetcher<Ad, TimelineError> // Service用のAdapterも用意する
init(timelineFetcher: AnyFetcher<Tweet, TimelineError>, adFetcher: AnyFetcher<Ad, TimelineError>) {
self.timelineFetcher = timelineFetcher
self.adFetcher = adFetcher
}
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void) {
// ..
}
}
・ Presenterを介させる
・ 表示にそのまま対応するデータ構
造を構築して、構築するロジック
だけテストできるようにクラスと
して切り出す
33.
[beta]
enum TimelineElement {
case tweet(tweet: Tweet)
case ad(ad: Ad)
}
protocol TimelinePresenterAdapter: class {
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void)
}
class TimelinePresenter<T, S>: TimelinePresenterAdapter {
fileprivate let timelineFetcher: AnyFetcher<Tweet, TimelineError> // Service用のAdapterも用意する
fileprivate let adFetcher: AnyFetcher<Ad, TimelineError> // Service用のAdapterも用意する
init(timelineFetcher: AnyFetcher<Tweet, TimelineError>, adFetcher: AnyFetcher<Ad, TimelineError>) {
self.timelineFetcher = timelineFetcher
self.adFetcher = adFetcher
}
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void) {
// ..
}
}
・ Presenterを介させる
・ 表示にそのまま対応するデータ構
造を構築して、構築するロジック
だけテストできるようにクラスと
して切り出す
34.
[beta]
enum TimelineElement {
case tweet(tweet: Tweet)
case ad(ad: Ad)
}
protocol TimelinePresenterAdapter: class {
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void)
}
class TimelinePresenter<T, S>: TimelinePresenterAdapter {
fileprivate let timelineFetcher: AnyFetcher<Tweet, TimelineError> // Service用のAdapterも用意する
fileprivate let adFetcher: AnyFetcher<Ad, TimelineError> // Service用のAdapterも用意する
init(timelineFetcher: AnyFetcher<Tweet, TimelineError>, adFetcher: AnyFetcher<Ad, TimelineError>) {
self.timelineFetcher = timelineFetcher
self.adFetcher = adFetcher
}
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void) {
// ..
}
}
・ Presenterを介させる
・ 表示にそのまま対応するデータ構
造を構築して、構築するロジック
だけテストできるようにクラスと
して切り出す
35.
[beta]
enum TimelineElement {
case tweet(tweet: Tweet)
case ad(ad: Ad)
}
protocol TimelinePresenterAdapter: class {
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void)
}
class TimelinePresenter<T, S>: TimelinePresenterAdapter {
fileprivate let timelineFetcher: AnyFetcher<Tweet, TimelineError> // Service用のAdapterも用意する
fileprivate let adFetcher: AnyFetcher<Ad, TimelineError> // Service用のAdapterも用意する
init(timelineFetcher: AnyFetcher<Tweet, TimelineError>, adFetcher: AnyFetcher<Ad, TimelineError>) {
self.timelineFetcher = timelineFetcher
self.adFetcher = adFetcher
}
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void) {
// ..
}
}
・ Presenterを介させる
・ 表示にそのまま対応するデータ構
造を構築して、構築するロジック
だけテストできるようにクラスと
して切り出す
36.
[beta]
enum TimelineElement {
case tweet(tweet: Tweet)
case ad(ad: Ad)
}
protocol TimelinePresenterAdapter: class {
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void)
}
class TimelinePresenter<T, S>: TimelinePresenterAdapter {
fileprivate let timelineFetcher: AnyFetcher<Tweet, TimelineError> // Service用のAdapterも用意する
fileprivate let adFetcher: AnyFetcher<Ad, TimelineError> // Service用のAdapterも用意する
init(timelineFetcher: AnyFetcher<Tweet, TimelineError>, adFetcher: AnyFetcher<Ad, TimelineError>) {
self.timelineFetcher = timelineFetcher
self.adFetcher = adFetcher
}
func provide(userId: String?, completeHandler: ([TimelineElement]) -> Void) {
// ..
}
}
・ Presenterを介させる
・ 表示にそのまま対応するデータ構
造を構築して、構築するロジック
だけテストできるようにクラスと
して切り出す
37.

UIView ↓ UIViewController ↑ Presenter Adapter ↑ Presenter ↑ Service Adapter ↑ Service ↑ Repository ↑ APIClient ↑ OAuth Library DB APIClient Adapter Design Object

38.

追加要件5 タイムライン ユーザー一覧 FUJI Goro "Go APIサーバーの設計について、 https://t.co/ naE0uvV6yn#9 で話しました。 - Gunosy Tech Blog" https://t.co/sdjN8pzo4M ボケて(人気) ギリギリ変身しなくても倒せそうな怪人で悩む https:// t.co/5NopBpbFSV sayoko RT @Haagen_Dazs_JP: 【@Haagen_Dazs_JP】を フォロー、この投稿をリツイートすると抽選で1名様に 新商品クリスピーサンド«パンプキンデイング &gt;&gt;100個セットをプレゼント♪ #パンプキン パーティー https://t.co/FC7Rw8qQlNy h... sayoko かわいすぎてやばい、、、 そっそーでダウンロードしたよね、、、 https://t.co/BC7Rw10ova https:// t.co/TpWTgDDF2u 席瀬 \(^o^)/ 雄大 RT @konifar: 金だよ金 Toshihiro Goto MS、儲けがないものに関してバッサリ切る感じになっ てきている。 多様な遷移元 多様な遷移先 ・ DeepLinkに対応したい。色んな 画面から画面遷移させたい。あ る遷移の文脈では別の流れを作 りたい

39.

UIView UIViewController Presenter Adapter Presenter Service Adapter Service Repository APIClient DB Adapter Design Object ・ Coordinatorパターンで遷移の依 存を切り出して、ViewController をCoordinatorでコンポジショ ンする

40.
[beta]
protocol Coordinator: class {
func start()
}
protocol NavigationCoordinator: Coordinator {
var navigationController: UINavigationController { get }
}
protocol UserTransition {
func show(userId: String)
}
extension UserTransition where Self: NavigationCoordinator {
func show(userId: String) {
let vc = UIStoryboard(name: "UserViewController", bundle: nil)
.instantiateInitialViewController() as! UserViewController
navigationController.pushViewController(vc, animated: true)
}
}
class WebviewCoordinator: NavigationCoordinator, UserTransition {
let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = UIStoryboard(name: "TimelineViewController", bundle: nil)
.instantiateInitialViewController() as! TimelineViewController
vc.didSelectTweet = { [weak self] (tweet) in
self?.show(userId: tweet.user.name)
}
navigationController.pushViewController(vc, animated: true)
}
}
・ Coordinatorパターンで遷移の依
存を切り出す
・ ViewControllerをCoordinator
でコンポジショ
ンする
41.
[beta]
protocol Coordinator: class {
func start()
}
protocol NavigationCoordinator: Coordinator {
var navigationController: UINavigationController { get }
}
protocol UserTransition {
func show(userId: String)
}
extension UserTransition where Self: NavigationCoordinator {
func show(userId: String) {
let vc = UIStoryboard(name: "UserViewController", bundle: nil)
.instantiateInitialViewController() as! UserViewController
navigationController.pushViewController(vc, animated: true)
}
}
class WebviewCoordinator: NavigationCoordinator, UserTransition {
let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = UIStoryboard(name: "TimelineViewController", bundle: nil)
.instantiateInitialViewController() as! TimelineViewController
vc.didSelectTweet = { [weak self] (tweet) in
self?.show(userId: tweet.user.name)
}
navigationController.pushViewController(vc, animated: true)
}
}
・ Coordinatorパターンで遷移の依
存を切り出す
・ ViewControllerをCoordinator
でコンポジショ
ンする
42.
[beta]
protocol Coordinator: class {
func start()
}
protocol NavigationCoordinator: Coordinator {
var navigationController: UINavigationController { get }
}
protocol UserTransition {
func show(userId: String)
}
extension UserTransition where Self: NavigationCoordinator {
func show(userId: String) {
let vc = UIStoryboard(name: "UserViewController", bundle: nil)
.instantiateInitialViewController() as! UserViewController
navigationController.pushViewController(vc, animated: true)
}
}
class WebviewCoordinator: NavigationCoordinator, UserTransition {
let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = UIStoryboard(name: "TimelineViewController", bundle: nil)
.instantiateInitialViewController() as! TimelineViewController
vc.didSelectTweet = { [weak self] (tweet) in
self?.show(userId: tweet.user.name)
}
navigationController.pushViewController(vc, animated: true)
}
}
・ Coordinatorパターンで遷移の依
存を切り出す
・ ViewControllerをCoordinator
でコンポジショ
ンする
43.
[beta]
protocol Coordinator: class {
func start()
}
protocol NavigationCoordinator: Coordinator {
var navigationController: UINavigationController { get }
}
protocol UserTransition {
func show(userId: String)
}
extension UserTransition where Self: NavigationCoordinator {
func show(userId: String) {
let vc = UIStoryboard(name: "UserViewController", bundle: nil)
.instantiateInitialViewController() as! UserViewController
navigationController.pushViewController(vc, animated: true)
}
}
class WebviewCoordinator: NavigationCoordinator, UserTransition {
let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = UIStoryboard(name: "TimelineViewController", bundle: nil)
.instantiateInitialViewController() as! TimelineViewController
vc.didSelectTweet = { [weak self] (tweet) in
self?.show(userId: tweet.user.name)
}
navigationController.pushViewController(vc, animated: true)
}
}
・ Coordinatorパターンで遷移の依
存を切り出す
・ ViewControllerをCoordinator
でコンポジショ
ンする
44.
[beta]
protocol Coordinator: class {
func start()
}
protocol NavigationCoordinator: Coordinator {
var navigationController: UINavigationController { get }
}
protocol UserTransition {
func show(userId: String)
}
extension UserTransition where Self: NavigationCoordinator {
func show(userId: String) {
let vc = UIStoryboard(name: "UserViewController", bundle: nil)
.instantiateInitialViewController() as! UserViewController
navigationController.pushViewController(vc, animated: true)
}
}
class WebviewCoordinator: NavigationCoordinator, UserTransition {
let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = UIStoryboard(name: "TimelineViewController", bundle: nil)
.instantiateInitialViewController() as! TimelineViewController
vc.didSelectTweet = { [weak self] (tweet) in
self?.show(userId: tweet.user.name)
}
navigationController.pushViewController(vc, animated: true)
}
}
・ Coordinatorパターンで遷移の依
存を切り出す
・ ViewControllerをCoordinator
でコンポジショ
ンする
45.

Coordinator ↓ Coordinator Adapter ↓ UIView ↓ UIViewController ↑ Presenter Adapter ↑ Presenter ↑ Service Adapter ↑ Service ↑ Repository ↑ APIClient ↑ OAuth Library DB APIClient Adapter Design Object

46.

どの課題にも共通していること ・ 依存をインターフェース経由で外へ出そう ・ 単一責務で分けてコンポジションしよう

47.

まとめ ・ クラス設計は組織やプロダクトの状況に合わせて自分たちで育てて いく(パターンを作っていく)のがよいのではないか ・ 目的を考え、原理原則を指針に、パターンは事例紹介 ・ 言語固有のテクニックや発展的な方法をそこへのせていく

48.

参考資料 ・ The Clean Architecture ・ https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html ・ iOS Project Architecture: Using VIPER ・ https://cheesecakelabs.com/blog/ios-project-architecture-using-viper/ ・ Presentation Model ・ https://martinfowler.com/eaaDev/PresentationModel.html ・ GUI Architectures ・ https://martinfowler.com/eaaDev/uiArchs.html ・ Ports-And-Adapters / Hexagonal Architecture ・ http://www.dossier-andreas.net/software_architecture/ports_and_adapters.html ・ Making Apps Adaptive, Part 2 ・ https://developer.apple.com/videos/play/wwdc2016/233/ ・ ユニバーサルアプリの理想と現実: Yahoo! JAPAN MeetUp #2 ・ https://speakerdeck.com/kazuhiro4949/yunibasaraupurifalseli-xiang-toxian-shi-yahoo-japan-meetup-number-2 ・ SwiftのDI方法について最近考えた話 - iOS LT #28 ・ https://speakerdeck.com/kazuhiro4949/swiftalsedifang-fa-nituitezui-jin-kao-etetahua-ios-lt-number-1 ・ Coordinatorパターンの実践 // Speaker Deck ・ https://speakerdeck.com/yoching/coordinatorpatanfalseshi-jian ・ Repository pattern in Swift // Speaker Deck ・ https://speakerdeck.com/naoty/repository-pattern-in-swift