Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Eight iOSアプリの開発手法 / Development method of Eight iOS App

Sansan
November 05, 2021

Eight iOSアプリの開発手法 / Development method of Eight iOS App

■イベント

Sansan Builders Stage 2021
https://jp.corp-sansan.com/engineering/buildersstage2021/

■登壇概要

タイトル:
Eight iOSアプリの開発手法
登壇者:技術本部 Mobile Applicationグループ  iOSエンジニア 小清水 健人

▼Sansan Engineering
https://jp.corp-sansan.com/engineering/

Sansan

November 05, 2021
Tweet

More Decks by Sansan

Other Decks in Technology

Transcript

  1. 技術本部 Mobile Applicationグループ iOSエンジニア ⾦融系SI、アプリ受託開発、EC事業会社を経て、 2021年4⽉ Sansan株式会社 中途⼊社。 Eight iOS

    アプリの開発に従事。 Eight ONAIR、Eight Marketing Solutions 刷新、ミニレポ などを開発。 ⼩清⽔ 健⼈
  2. • Eight について • 設計 • アーキテクチャ • Eight iOS

    の実装例 セッション概要 Table of contents
  3. 名刺を管理する機能以外にもさまざまな機能 プロフィール 検索 メッセージ フィード( 投稿 ) 連絡先 / 会社

    名刺撮影 ONAIR お知らせ Eight について 名刺でつながる、 ビジネスのためのSNS
  4. Eight について Eight iOS アプリの規模感 • Compile Sources:2200 + •

    Swift 86 % • Objective-C 14 % • Bundle Resources 600 +
  5. PlantUML を⽤いてクラス図に落とし込みます。 Eight iOS では『 MVVM + Coordinator 』パターンを 採⽤しています。このパターンに則り、プロトコル、

    クラス、構造体などのインターフェースやデータ構造 をすべてクラス図として落とし込みます。 設計フェーズ クラス図
  6. View はデータの表⽰と UI イベントを送信するクラスで す。 UIViewController が View の責務を負います。 ViewModel

    とデータバインディングすることでデータを 表⽰します。 Eight iOS では UI イベントの受信先のクラスを View か ら隠蔽します。データバインディング機構と UI イベント の発⾏を外部公開する設計としています。 アーキテクチャ View ViewModel View ( UIViewController ) UI Event View item
  7. ViewModel は View と Model の仲介者です。 Model にデータの取得の依頼とデータの取得・変更の検 知をします。取得したドメインオブジェクトを 表⽰⽤オ

    ブジェクトに変換して View へ発⾏します。 また、画⾯遷移のためのイベントを発⾏します。 画⾯遷移イベントは Coordinator が購読して画⾯遷移を 解決します。 アーキテクチャ ViewModel Model ( UseCase ) ViewModel Request Domain object
  8. Model は ViewModel 依頼を受けてデータの取得を⾏います。 Eight iOS では、UseCase クラスが ViewModel に対して、

    ドメインの値型オブジェクトを提供します。 アーキテクチャ Model Model ( UseCase )
  9. アーキテクチャ Eight の Model ViewModel UseCase API Client Realm Manager

    ViewModel がデータの取得を UseCase に要求
  10. アーキテクチャ Eight の Model ViewModel UseCase API Client Realm Manager

    取得したデータを Realm で永続化 Save as Realm Objects
  11. アーキテクチャ Eight の Model ViewModel UseCase API Client Realm Manager

    データの取得完了をイベントとして発⾏ RxSwift.Completable で API の成功・失敗を伝搬
  12. アーキテクチャ Eight の Model ViewModel UseCase API Client Realm Manager

    Realm オブジェクトの追加・更新・削除を UseCase が購読 ドメインオブジェクトに変換して ViewModel に⾮同期に発⾏ Restore and convert to Domain Objects
  13. Eight iOS では画⾯遷移の処理は Coordinator クラス が担当します。ViewModel が発⾏する画⾯遷移のイベ ントを受け取り、次に表⽰する ViewController に遷

    移します。 画⾯遷移の処理を ViewController から Coordinator に委譲することで、ViewController は MVVM アーキ テクチャの View の責務に徹することができます。 アーキテクチャ Coordinator パターン Coordinator
  14. • Web API で イベントの⼀覧を取得 • 取得したデータをキャッシュとして Realm に保存 •

    リストにデータを表⽰ • リスト要素のタップでイベントの詳細に遷移 Eight iOS の実装例 ONAIR 中のイベント
  15. @objcMembers class OnairEvent: RLMObject { dynamic var eventID = 0

    dynamic var title = "" dynamic var descriptionString = "" dynamic var url = "" dynamic var startAt = Date() override class func primaryKey() -> String { "eventID" } } Eight iOS の実装例 Model Realm Object
  16. Eight iOS の実装例 Model API Request struct GetOnairOnairEventsRequest: RequestType {

    let method = HTTPMethod.GET let path = "/onair/onair_events" func responseFromObject(_ object: AnyObject) -> [OnairEvent]? { guard let response = object as? [String: Any], let events = response["events"] as? [[String: Any]] else { return nil } return events.compactMap(OnairEvent.init) } }
  17. Eight iOS の実装例 Model Domain Object struct Event: Equatable {

    let eventID: EventID let title: String let description: String let url: URL let startAt: Date }
  18. Fetchable RxSwift.Completable で Web API の取得の完了 or 失敗を表現します。 import RxSwift

    public protocol EventsFetchable { func fetch() -> Completable } Eight iOS の実装例
  19. final class OnairEventsUseCase: EventsFetchable, EventsLoadable private let realmManager: RealmManaging private

    let apiClient: APIClientType init(realmManager: RealmManaging, apiClient: APIClientType) { self.realmManager = realmManager self.apiClient = apiClient } func fetch() -> Completable { fatalError(”TODO: Implementation") } func load() -> Observable<EventList> { fatalError(”TODO: Implementation") } } Eight iOS の実装例
  20. func fetch() -> Completable { apiClient .rx_request(GetOnairOnairEventsRequest()) .flatMapCompletable { [realmManager]

    events -> Completable in realmManager.execute { realm in realm.addOrUpdateObjects(events) } } } Eight iOS の実装例
  21. func load() -> Observable<[Event]> { realmManager.readAndObserveUpdate { realm in OnairEvent.allObjects(in:

    realm) }.map { onairEvent in onairEvent.map(Event.init) } } Eight iOS の実装例
  22. ViewStates, NavigationEvents のプロトコルに適合します。 - ViewStates:表⽰データを View に提供 - NavigationEvents:遷移イベントを Coordinator

    に提供 ViewModel は UseCase の具体型ではなく、 その抽象である Fetchable と Loadable に依存します。 Domain Object を View 表⽰⽤オブジェクトに編集します。 ViewModel View Model View States Navigation Events Fetchable Loadable Eight iOS の実装例
  23. ViewItem View 表⽰⽤のデータ構造 struct EventListItem { let eventID: EventID let

    timeString: String let titleString: String } Eight iOS の実装例
  24. ViewStates RxSwift.Driver で View 表⽰⽤のデータを提供 import RxSwift import RxCocoa protocol

    EventsViewStates { var items: Driver<[EventListItem]> { get } } Eight iOS の実装例
  25. NavigationEvents RxSwift.Driver で画⾯遷移イベントを発⾏ import RxSwift import RxCocoa protocol EventsNavigationEvents {

    var showEventDetail: Driver<EventID> { get } var showError: Driver<Void> { get } } Eight iOS の実装例
  26. final class EventsViewModel: EventsViewStates, EventsNavigationEvents { let items: Driver<[EventListItem]> let

    showEventDetail: Driver<EventID> let showError: Driver<Void> struct Dependency { let fetcher: EventsFetchable let loader: EventsLoadable } struct UIEvents { let eventTapped: Observable<EventID> let viewWillAppear: Observable<Void> } init(dependency: Dependency, events: UIEvents) { fatalError("TODO: Implementation") } } Eight iOS の実装例
  27. init(dependency: Dependency, events: UIEvents) { self.showError = events .viewWillAppear .flatMapFirst

    { dependency.fetcher.fetch() .andThen(Observable.just(())) .materialize() .compactMap { $0.error } } .map { _ in () } .asDriver(onErrorDriveWith: .empty()) self.items = dependency.loader.load() .map { $0.map(EventListItem.init(event:)) } .asDriver(onErrorDriveWith: .empty()) self.showEventDetail = events.eventTapped .asDriver(onErrorDriveWith: .empty()) } Eight iOS の実装例
  28. import UIKit import RxSwift import RxCocoa final class EventsViewController: UIViewController

    { @IBOutlet private weak var tableView: UITableView! private var items: [EventListItem] = [] private let disposeBag = DisposeBag() var eventTapped: Observable<EventID> { fatalError("TODO: Implementation") } var viewWillAppear: Observable<Void> { fatalError("TODO: Implementation") } func bind(viewStates: EventListViewStates) { fatalError("TODO: Implementation") } } Eight iOS の実装例
  29. var eventTapped: Observable<EventID> { tableView.rx.itemSelected.compactMap { [weak self] indexPath in

    self?.items[indexPath.row].eventID } } var viewWillAppear: Observable<Void> { self.rx.methodInvoked(#selector(viewWillAppear(_:))) .map { _ in () } } func bind(viewStates: EventListViewStates) { viewStates.items.drive(onNext: { [weak self] items in self?.items = items self?.tableView.reloadData() }) .disposed(by: disposeBag) } Eight iOS の実装例
  30. final class EventListCoordinator { private weak var parent: UIViewController? private

    let disposeBag = DisposeBag() init(parent: UIViewController) { self.parent = parent } func start() { // ⼀覧を表⽰ fatalError("TODO: Implement") } private func bind(navigationEvents: EventListNavigationEvents) { // 詳細を表⽰ fatalError("TODO: Implement") } } Eight iOS の実装例
  31. Eight iOS の実装例 func start() { let (viewController, viewStates, navigationEvents)

    = makeEventList() viewController.bind(viewStates: viewStates) bind(navigationEvents: navigationEvents) parent?.show(viewController, sender: nil) }
  32. private func makeEventList() -> ( EventsViewController, EventsViewStates, EventsNavigationEvents ) {

    // instantiate from storyboard. let controller: EventsViewController = // ... controller.loadViewIfNeeded() let viewModel = EventsViewModel( dependency: .init( fetcher: Container.sharedResolver.resolve(EventsFetchable.self)!, loader: Container.sharedResolver.resolve(EventsLoadable.self)! ), events: .init( eventTapped: controller.eventTapped, viewWillAppear: controller.viewWillAppear ) ) return (controller, viewModel, viewModel) } Eight iOS の実装例
  33. Eight iOS の実装例 private func bind(navigationEvents: EventListNavigationEvents) { navigationEvents .showEventDetail

    .drive(onNext: { [weak self] eventID in self?.showEventDetail(eventID: eventID) }) .disposed(by: disposeBag) }