Save 37% off PRO during our Black Friday Sale! »

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

13d936e697fe0f4fa96f926d0a712f6c?s=47 Sansan
PRO
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/

13d936e697fe0f4fa96f926d0a712f6c?s=128

Sansan
PRO

November 05, 2021
Tweet

Transcript

  1. STAGE 1 iOS Engineer SESSION TAG ⼩清⽔ 健⼈ Eight iOS

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

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

    の実装例 セッション概要 Table of contents
  4. Eight について

  5. 名刺を管理する機能以外にもさまざまな機能 プロフィール 検索 メッセージ フィード( 投稿 ) 連絡先 / 会社

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

    Swift 86 % • Objective-C 14 % • Bundle Resources 600 +
  7. • Eight では1週間のスプリントでスクラム開発をしています。 • スプリント計画 • 開発( 設計/実装 ) •

    スプリントレビュー Eight について スクラム開発
  8. 設計フェーズ

  9. • Eight iOS チームでは次のように設計を進めます。 • クラス図 • 設計レビュー • インターフェースの実装

    設計フェーズ 設計
  10. PlantUML を⽤いてクラス図に落とし込みます。 Eight iOS では『 MVVM + Coordinator 』パターンを 採⽤しています。このパターンに則り、プロトコル、

    クラス、構造体などのインターフェースやデータ構造 をすべてクラス図として落とし込みます。 設計フェーズ クラス図
  11. 設計フェーズ

  12. • iOS の開発メンバー全員で対⾯形式(オフライン / オンライン)で実施 • インターフェースに過不⾜がないか • 実装の⽅法の事前の認識合わせ •

    開発メンバーからのフィードバックをもとに設計を決定 設計フェーズ 設計レビュー
  13. 設計を実装します。実際に設計を実装することで、各クラスのインターフェースに問題がない か明らかにします。必要なクラスなどを開発前段階で事前にコミット、develop ブランチに マージすることで、開発メンバーはクラス単位で開発をすすめることができます。 設計フェーズ インターフェースの実装

  14. アーキテクチャ

  15. アーキテクチャ MVVM + Coordinator パターン Model ViewModel View Coordinator

  16. View はデータの表⽰と UI イベントを送信するクラスで す。 UIViewController が View の責務を負います。 ViewModel

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

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

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

  20. アーキテクチャ Eight の Model ViewModel UseCase API Client Realm Manager

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

    UseCase が Web API 経由でデータを取得
  22. アーキテクチャ Eight の Model ViewModel UseCase API Client Realm Manager

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

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

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

    移します。 画⾯遷移の処理を ViewController から Coordinator に委譲することで、ViewController は MVVM アーキ テクチャの View の責務に徹することができます。 アーキテクチャ Coordinator パターン Coordinator
  26. アーキテクチャ Coordinator パターン Coordinator A ViewModel A Coordinator B ViewController

    B Navigation Event Present Start
  27. Eight iOS の実装例

  28. • Web API で イベントの⼀覧を取得 • 取得したデータをキャッシュとして Realm に保存 •

    リストにデータを表⽰ • リスト要素のタップでイベントの詳細に遷移 Eight iOS の実装例 ONAIR 中のイベント
  29. Eight iOS の実装例 Model Domain Object API Request Realm Object

    UseCase
  30. @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
  31. 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) } }
  32. Eight iOS の実装例 Model Domain Object struct Event: Equatable {

    let eventID: EventID let title: String let description: String let url: URL let startAt: Date }
  33. 公開インターフェースとして Fetchable, Loadable のプロト コルを定義し、UseCase がプロトコルに適合します。 - Fetchable:取得処理を提供 - Loadable:キャッシュ復元処理を提供

    UseCase UseCase Fetchable Loadable Eight iOS の実装例
  34. Fetchable RxSwift.Completable で Web API の取得の完了 or 失敗を表現します。 import RxSwift

    public protocol EventsFetchable { func fetch() -> Completable } Eight iOS の実装例
  35. Loadable RxSwift.Observable で Realm Object の追加・更新時にドメインオブジェクトを発⾏します。 import RxSwift public protocol

    EventsLoadable { func load() -> Observable<[Event]> } Eight iOS の実装例
  36. 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 の実装例
  37. func fetch() -> Completable { apiClient .rx_request(GetOnairOnairEventsRequest()) .flatMapCompletable { [realmManager]

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

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

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

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

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

    var showEventDetail: Driver<EventID> { get } var showError: Driver<Void> { get } } Eight iOS の実装例
  43. 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 の実装例
  44. 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 の実装例
  45. ViewModel の抽象である ViewStates に依存します。 ViewStates が発⾏する表⽰⽤の値を ViewController でデータバインディングします。 View View

    ( UIViewController ) ViewStates subcribe publisch Eight iOS の実装例
  46. 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 の実装例
  47. 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 の実装例
  48. NavigationEvents に依存します。 NavigationEvents が発⾏する画⾯遷移のイベントを 監視して、次の画⾯を表⽰します。 Eight iOS の実装例 Coordinator Coordinator

    NavigationEvents subcribe publisch
  49. 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 の実装例
  50. Eight iOS の実装例 func start() { let (viewController, viewStates, navigationEvents)

    = makeEventList() viewController.bind(viewStates: viewStates) bind(navigationEvents: navigationEvents) parent?.show(viewController, sender: nil) }
  51. 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 の実装例
  52. Eight iOS の実装例 private func bind(navigationEvents: EventListNavigationEvents) { navigationEvents .showEventDetail

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

  54. • Eight では1週間のスプリントで開発 • 設計フェーズでクラス図を作成 • 内部実装の前段階で空のクラスをコミット • 実装フェーズはクラス単位で並⾏開発 •

    MVVM + Coordinator パターンで責務を明確化 まとめ Eight iOS アプリの開発⼿法
  55. iOS Engineer Twitter @_take_hito_ VirtualCard ⼩清⽔ 健⼈