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

Wantedly People ViewModel and Rx

Wantedly People ViewModel and Rx

Avatar for yohei sugigami

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)