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. 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") } }
  2. 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() ) ) }() }
  3. 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) { ... } }
  4. 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) } }
  5. 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) } ... } }
  6. 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) } }
  7. ิ଍ 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
  8. ิ଍ 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
  9. 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 () } } }
  10. 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> ... }
  11. 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) } }
  12. 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
  13. 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) } } } } } }
  14. 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 () } } }
  15. 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)