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

Wantedly People ViewModel and Rx

Wantedly People ViewModel and Rx

yohei sugigami

May 31, 2017
Tweet

More Decks by yohei sugigami

Other Decks in Technology

Transcript

  1. None
  2. None
  3. None
  4. None
  5. Agenda • ViewModelͷΠχγϟϥΠζ • ViewModelͷೖྗਫ਼ࠪ • ViewModelͷ௨৴ॲཧ • ௨৴தͱϦτϥΠΤϥʔϋϯυϦϯάͷঢ়ଶ؅ཧ •

    Realmத৺ઃܭ Disclaimer αϯϓϧίʔυ͸Swift 2.3 !
  6. ɹ ɹ ViewModelͷΠχγϟϥΠζ

  7. ViewModelͷΠχγϟϥΠζ ΠϯϓοτͱͳΔঢ়ଶมԽ(Driver)͸ϓϩύςΟʹͤͣΠχγϟϥ ΠβͰ୅ೖͯ͠ɺUIViewController͔ΒόΠϯσΟϯάͷ࿙ΕΛ๷ ࢭͱɺॳظԽ࣌ʹඞཁͳ৘ใͱͯ͠໌จԽɻ class WorkingHistoriesViewModel { init(input: ( companyName:

    Driver<String>, position: Driver<String>, saveTaps: Driver<Void> ), dependencies: Dependencies = DefaultDependencies.sharedInstance) { ... } }
  8. ViewModelͷ֎෦ཁҼΛૄ݁߹ʹʢґଘੑͷ஫ೖʣ ςετ࣌ʹ֎෦ཁҼΛ༰қʹελϒ΁੾Γସ͑Ͱ͖ΔΑ͏ґଘ͢ ΔΦϒδΣΫτΛϓϩτίϧԽͯ͠ɺςετ࣌ʹ࣮૷Λࠩ͠ସ͑ Մೳʹ͢Δɻ class WorkingHistoriesViewModel { init(input: ( companyName:

    Driver<String>, position: Driver<String>, saveTaps: Driver<Void> ), dependencies: Dependencies = DefaultDependencies.sharedInstance) { ... } }
  9. ViewModelͷ֎෦ཁҼΛૄ݁߹ʹʢґଘੑͷ஫ೖʣ protocol Dependencies { var session: Session { get }

    // APIKit var wireframe: Wireframe { get } var validationService: ValidationService { get } var reachability: Reachability { get } } class DefaultDependencies: Dependencies { static let sharedInstance = DefaultDependencies() let session: Session let wireframe: Wireframe let validationService: DefaultValidationService let reachability: Reachability private init() { session = Session() wireframe = DefaultWireframe() validationService = ValidationService() reachability = try! Reachability(hostname: "some.host") } }
  10. ViewModelͷΠϯελϯԽ ϏϡʔͷΠϯελϯε͕ॳظԽ͞ΕͨޙͰͳ͍ͱDriverΛऔಘͰ͖ ͳ͍ͷͰlazyͰ஗ԆධՁɻ class WorkingHistoriesViewController { lazy var viewModel: WorkingHistoriesViewModel

    = { return WorkingHistoriesViewModel( input: ( companyName: self.companyNameTextField.rx_text.asDriver(), position: self.positionTextField.rx_text.asDriver(), saveTaps: self.saveBarButton.rx.asDriver() ) ) }() }
  11. ViewModelͱUIViewControllerΛૄ݁߹ʹ͢Δ৔߹ UIViewControllerͱViewModelΛૄ݁߹ʹ͍ͨ͠ʢVCͷςετΛॻ ͖͍ͨͳͲʣ৔߹͸ɺΠχγϟϥΠβͰ୅ೖͤͣPublishSubjectΛ ϓϩύςΟͱͯ͠ఆٛͯ͠όΠϯσΟϯάɻ class WorkingHistoriesViewModel { let companyName =

    PublishSubject<String>() let position = PublishSubject<String>() let saveTaps = PublishSubject<Void>() init(dependencies: Dependencies = DefaultDependencies.sharedInstance) { ... } }
  12. ViewModelͷΠϯελϯԽ(ૄ݁߹ʹ͢Δ৔߹) UIViewControllerͷΠχγϟϥΠζͰViewModelΛ୅ೖɻςετ࣌͸ ελϒͳViewModelʹࠩ͠ସ͑ՄೳͱͳΔɻ class WorkingHistoriesViewController { let model: WorkingHistoriesViewModel init(model:

    WorkingHistoriesViewModel = WorkingHistoriesViewModel()) { self.model = model } override func viewDidLoad() { super.viewDidLoad() companyNameTextField.rx_text.asDriver().drive(companyName).addDisposableTo(disposeBag) positionTextField.rx_text.asDriver().drive(position).addDisposableTo(disposeBag) saveBarButton.rx.asDriver().drive(saveTaps).addDisposableTo(disposeBag) } }
  13. ɹ ɹ ViewModelͷೖྗਫ਼ࠪ

  14. ViewModelͷೖྗਫ਼ࠪ ೖྗਫ਼ࠪΛετϦʔϜ಺Ͱ࣮ࢪ͢Δ͜ͱ Ͱɺೖྗঢ়ଶͷมԽʢϢʔβ͕จࣈΛ౎ ౓ೖྗ͢Δ౓ʣʹϦΞϧλΠϜೖྗਫ਼ࠪ Λߦ͏ɻਫ਼ࠪͷ݁Ռ΋DriverͰެ։͢Δ ͜ͱͰɺUIViewContoller্ͰϦΞϧλΠ Ϝʹ݁ՌΛදࣔͰ͖Δɻ

  15. class WorkingHistoriesViewModel { let validatedCompanyName: Driver<ValidationResult> let validatedPosition: Driver<ValidationResult> let

    saveEnabled: Driver<Bool> init(input: (companyName: Driver<String>, position: Driver<String>, saveTaps: Driver<Void>), dependencies: Dependencies = DefaultDependencies.sharedInstance) { // Normalization let companyNameNormalized = input.companyName.map { $0.trim() } let positionNormalized = input.position.map { $0.trim() } // Validation self.validatedCompanyName = companyNameNormalized.map { dependencies.validationService.validateNotEmpty($0) } self.validatedPosition = positionNormalized.map { dependencies.validationService.validateNotEmpty($0) } self.saveEnabled = Driver.combineLatest(validatedCompanyName, validatedPosition) { $0.isValid && $1.isValid } // Request let combineRequest = Driver.combineLatest(companyNameNormalized, positionNormalized) { ($0, $1) } self.responseSuccessed = input.saveTaps .withLatestFrom(combineRequest) .flatMapLatest { API.Endpoint.AccountProfilePatchRequest($0) } ... } }
  16. UIViewControllerͷೖྗਫ਼ࠪόΠϯσΟϯά class WorkingHistoriesViewController { override func viewDidLoad() { super.viewDidLoad() //

    Validation viewModel.validatedCompanyName .drive(validationCompanyNameLabel.rx.validationResult).addDisposableTo(disposeBag) viewModel.validatedPosition .drive(validationPositionLabel.rx.validationResult).addDisposableTo(disposeBag) viewModel.saveEnabled .drive(saveBarButton.rx_enabled).addDisposableTo(disposeBag) // Request viewModel.responseSuccessed .drive(rx.dismissViewController).addDisposableTo(disposeBag) } }
  17. ิ଍ ValidationResult1 ೖྗਫ਼ࠪͷ݁ՌΛenumͰఆٛɻ enum ValidationResult { case ok(message: String) case

    empty case validating case failed(message: String) } extension ValidationResult: CustomStringConvertible { var description: String { switch self { case let .ok(message): return message case .empty: return "ະೖྗͰ͢" case .validating: return "..." case let .failed(message): return message } } } 1 https:/ /github.com/ReactiveX/RxSwift/blob/900035d78b37e440b9098d0ffac28e0d8b8cc660/RxExample/RxExample/Examples/GitHubSignup/ BindingExtensions.swift
  18. ิ଍ ValidationResult1 ݁ՌΛUILabelʹόΠϯσΟϯάͯ͠දࣔ͢ΔͨΊʹObserverΛ֦ ுఆٛɻ extension Reactive where Base: UILabel {

    var validationResult: AnyObserver<ValidationResult> { return UIBindingObserver(UIElement: base) { label, result in label.textColor = result.textColor label.text = result.description }.asObserver() } } 1 https:/ /github.com/ReactiveX/RxSwift/blob/900035d78b37e440b9098d0ffac28e0d8b8cc660/RxExample/RxExample/Examples/GitHubSignup/ BindingExtensions.swift
  19. ɹ ɹ ViewModelͷ௨৴ॲཧ ʢ௨৴தͱϦτϥΠΤϥʔϋϯυϦϯάʣ

  20. None
  21. class WorkingHistoriesViewModel { let responseSuccessed: Driver<Void> init(input: (companyName: Driver<String>, position:

    Driver<String>, saveTaps: Driver<Void>), dependencies: Dependencies = DefaultDependencies.sharedInstance) { // Normalization ... // Validation ... // Request let wireframe = dependencies.wireframe let activityIndicator = ActivityIndicator(); wireframe.progress(tr(.NetworkLoading), activityIndicator: activityIndicator) self.responseSuccessed = input.saveTaps .withLatestFrom(combineRequest) .flatMapLatest { dependencies.session.rx_response(WorkingHistoriesRequest($0)) .retryWhenConfirmRetryAlert(wireframe) .do(onNext: { UserRealm.save($0) }) .trackActivity(activityIndicator) .asDriver(onErrorDriveWith: Driver.never()) } .map { _ in () } } }
  22. ViewModelͷ௨৴ॲཧʢ௨৴தͱϦτϥΠΤϥʔϋϯυϦϯάʣ ௨৴த΍ϦτϥΠͷΞϥʔτ͸Viewίϯϙʔωϯτ2ͷͨΊɺ੹຿ ͱͯ͠͸ViewϨΠϠʔʹͳΔ͕ViewModelʹهड़ͨ͠΄͏͕ɺ௨৴ த΍ҟৗܥͷঢ়ଶʹΑΔ෼ذॲཧͷهड़͕෼அ͞ΕͣɺViewModel ʹҰݩతʹهड़͕Ͱ͖ॲཧͷશମ૾Λ೺Ѳ͠΍͘͢ͳΔɻ ॲཧ݁Ռ͸ਖ਼ৗܥͷΈʹͳΔͨΊɺUIViewControllerʹରͯ͠ҟৗ ܥΛอ࣋Ͱ͖ͳ͍DriverͰฦͤΔɻ 2 UIActivityIndicatorView, UIAlertController

  23. Wireframe ViperΞʔΩςΫνϟͰ͸ɺWireframeΦϒδΣΫτ͸UIWindowɺ UINavigationControllerɺUIViewControllerͳͲΛॴ༗ͯ͠ϧʔςΟϯ άΛ୲͏ɻUIWindowΛࢀরͰ͖Δ͜ͱʹண໨ͯ͠ViewModel͔Β Viewʹର͢Δڞ௨తؔ৺ࣄͷૢ࡞ʹར༻ɻ protocol Wireframe { func progress(text:

    String, activityIndicator: ActivityIndicator?) func promptFor<Action: CustomStringConvertible>(title title: String, message: String, cancelAction: Action, actions: [Action]) -> Observable<Action> ... }
  24. ActivityIndicator class DefaultWireframe: Wireframe { func progress(text: String, activityIndicator: ActivityIndicator)

    { let progress = MBProgressHUD() progress.mode = MBProgressHUDMode.Indeterminate progress.label.text = text activityIndicator .asDriver() .bindTo(progress.rx.mbprogresshudAnimating) .addDisposableTo(disposeBag) } }
  25. ActivityIndicator3 class ActivityIndicator: DriverConvertibleType { private let variable = Variable(0)

    private let loading: Driver<Bool> init() { loading = variable.asObservable() .map { $0 > 0 } .distinctUntilChanged() .asDriver(onErrorRecover: ActivityIndicator.ifItStillErrors) } func trackActivity<O: ObservableConvertibleType>(source: O) -> Observable<O.E> { return Observable.using({ () -> ActivityToken<O.E> in self.increment() return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) }) { t in t.asObservable() } } func asDriver() -> Driver<E> { return loading } public func terminate() { private func increment() { variable.value = variable.value + 1 } private func decrement() { variable.value = variable.value - 1 } } 3 https:/ /raw.githubusercontent.com/ReactiveX/RxSwift/900035d78b37e440b9098d0ffac28e0d8b8cc660/RxExample/RxExample/Services/ ActivityIndicator.swift
  26. RetryWhenConfirmRetryAlert RxSwiftͷRetryWhenΦϖϨʔλʔΛ֦ுͯ͠೚ҙͷΤϥʔͷ৔߹ ͷΈɺUIAlertͰϦτϥΠ͢ΔॲཧΛΦϖϨʔλʔԽɻUIAlert΋Rx ͢Δ͜ͱͰɺϘλϯԡԼͷϢʔβΛૢ࡞ΛετϦʔϜʹγʔϜϨ εʹ݁߹ɻ

  27. RetryWhenConfirmRetryAlert extension ObservableType { func retryWhenConfirmRetryAlert(wireframe: Wireframe) -> Observable<Self.E> {

    return retryWhen { (errors: Observable<ErrorType>) -> Observable<PromptRetryAction> in return errors.flatMapWithIndex { (error, count) -> Observable<PromptRetryAction> in let (title, message, canRetry) = createErrorAlertMessage(error) if canRetry { let retry = PromptAction(tr(.NetworkRetry)) return wireframe.promptFor(title: title,ɹmessage: message,ɹcancelAction: PromptAction(tr(.GlobalCancel)), actions: [retry]).flatMap { (action) -> Observable<PromptRetryAction> in return action == retry ? Observable.just(PromptRetryAction()) : Observable.error(Error.Ignore) } } else { return wireframe.promptFor(title: title,ɹmessage: message, cancelAction: PromptAction(tr(.GlobalCancel)), actions: []).flatMap { (action) -> Observable<PromptRetryAction> in return Observable.error(Error.Ignore) } } } } } }
  28. ɹ ɹ Realmத৺ઃܭ

  29. class WorkingHistoriesViewModel { let responseSuccessed: Driver<Void> init(input: (companyName: Driver<String>, position:

    Driver<String>, saveTaps: Driver<Void>), dependencies: Dependencies = DefaultDependencies.sharedInstance) { // Normalization ... // Validation ... // Request let wireframe = dependencies.wireframe let activityIndicator = ActivityIndicator(); wireframe.progress(tr(.NetworkLoading), activityIndicator: activityIndicator) self.responseSuccessed = input.saveTaps .withLatestFrom(combine) .flatMapLatest { dependencies.session.rx_response(Request($0)) .retryWhenConfirmRetryAlert(wireframe) .do(onNext: { UserRealm.save($0) }) .trackActivity(activityIndicator) .asDriver(onErrorDriveWith: Driver.never()) } .map { _ in () } } }
  30. None
  31. None
  32. None
  33. RxRealm RealmNotificationΛRxԽͯ͠UIViewControllerͰsubscribe͢Δɻ try! Realm().objects(UserRealm) .asObservableChangeset() .subscribeNext { [weak self] results,

    changeset in guard let tableView = self?.tableView else { return } self?.results = results if let changeset = changeset { tableView.beginUpdates() tableView.insertRowsAtIndexPaths(…, withRowAnimation: .Automatic) tableView.deleteRowsAtIndexPaths(…, withRowAnimation: .Automatic) tableView.reloadRowsAtIndexPaths(…, withRowAnimation: .Automatic) tableView.endUpdates() } else { tableView.reloadData() } }.addDisposableTo(disposeBag)
  34. Conclusion • MVVMΞʔΩςΫνϟઃܭͷ۷ΓԼ͛ͯɺ۩ମతͳ࣮૷ྫͱ RxSwiftͷಛੑΛ׆͔ͨ͠ετϦʔϜͷهड़Λ঺հɻ • MVVMΞʔΩςΫνϟͰ೰Έ͕ͪͳɺΞϓϦέʔγϣϯશମʹ ԣஅతʹӨڹ͢Δঢ়ଶมԽʹରͯ͠ɺRealmΛ׆༻͢Δํ๏Λ ঺հɻ • ৄࡉ͸ҎલొஃࢿྉʮRealm

    Centered DesignʯΛࢀߟɻ