$30 off During Our Annual Pro Sale. View Details »

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. View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. Agenda
    • ViewModelͷΠχγϟϥΠζ
    • ViewModelͷೖྗਫ਼ࠪ
    • ViewModelͷ௨৴ॲཧ
    • ௨৴தͱϦτϥΠΤϥʔϋϯυϦϯάͷঢ়ଶ؅ཧ
    • Realmத৺ઃܭ
    Disclaimer
    αϯϓϧίʔυ͸Swift 2.3 !

    View Slide

  6. ɹ
    ɹ
    ViewModelͷΠχγϟϥΠζ

    View Slide

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

    View Slide

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

    View Slide

  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")
    }
    }

    View Slide

  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()
    )
    )
    }()
    }

    View Slide

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

    View Slide

  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)
    }
    }

    View Slide

  13. ɹ
    ɹ
    ViewModelͷೖྗਫ਼ࠪ

    View Slide

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

    View Slide

  15. class WorkingHistoriesViewModel {
    let validatedCompanyName: Driver
    let validatedPosition: Driver
    let saveEnabled: Driver
    init(input: (companyName: Driver, position: Driver, saveTaps: Driver),
    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) }
    ...
    }
    }

    View Slide

  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)
    }
    }

    View Slide

  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

    View Slide

  18. ิ଍ ValidationResult1
    ݁ՌΛUILabelʹόΠϯσΟϯάͯ͠දࣔ͢ΔͨΊʹObserverΛ֦
    ுఆٛɻ
    extension Reactive where Base: UILabel {
    var validationResult: AnyObserver {
    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

    View Slide

  19. ɹ
    ɹ
    ViewModelͷ௨৴ॲཧ
    ʢ௨৴தͱϦτϥΠΤϥʔϋϯυϦϯάʣ

    View Slide

  20. View Slide

  21. class WorkingHistoriesViewModel {
    let responseSuccessed: Driver
    init(input: (companyName: Driver, position: Driver, saveTaps: Driver),
    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 () }
    }
    }

    View Slide

  22. ViewModelͷ௨৴ॲཧʢ௨৴தͱϦτϥΠΤϥʔϋϯυϦϯάʣ
    ௨৴த΍ϦτϥΠͷΞϥʔτ͸Viewίϯϙʔωϯτ2ͷͨΊɺ੹຿
    ͱͯ͠͸ViewϨΠϠʔʹͳΔ͕ViewModelʹهड़ͨ͠΄͏͕ɺ௨৴
    த΍ҟৗܥͷঢ়ଶʹΑΔ෼ذॲཧͷهड़͕෼அ͞ΕͣɺViewModel
    ʹҰݩతʹهड़͕Ͱ͖ॲཧͷશମ૾Λ೺Ѳ͠΍͘͢ͳΔɻ
    ॲཧ݁Ռ͸ਖ਼ৗܥͷΈʹͳΔͨΊɺUIViewControllerʹରͯ͠ҟৗ
    ܥΛอ࣋Ͱ͖ͳ͍DriverͰฦͤΔɻ
    2 UIActivityIndicatorView, UIAlertController

    View Slide

  23. Wireframe
    ViperΞʔΩςΫνϟͰ͸ɺWireframeΦϒδΣΫτ͸UIWindowɺ
    UINavigationControllerɺUIViewControllerͳͲΛॴ༗ͯ͠ϧʔςΟϯ
    άΛ୲͏ɻUIWindowΛࢀরͰ͖Δ͜ͱʹண໨ͯ͠ViewModel͔Β
    Viewʹର͢Δڞ௨తؔ৺ࣄͷૢ࡞ʹར༻ɻ
    protocol Wireframe {
    func progress(text: String, activityIndicator: ActivityIndicator?)
    func promptFor(title title: String,
    message: String, cancelAction: Action, actions: [Action]) -> Observable
    ...
    }

    View Slide

  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)
    }
    }

    View Slide

  25. ActivityIndicator3
    class ActivityIndicator: DriverConvertibleType {
    private let variable = Variable(0)
    private let loading: Driver
    init() {
    loading = variable.asObservable()
    .map { $0 > 0 }
    .distinctUntilChanged()
    .asDriver(onErrorRecover: ActivityIndicator.ifItStillErrors)
    }
    func trackActivity(source: O) -> Observable {
    return Observable.using({ () -> ActivityToken in
    self.increment()
    return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
    }) { t in t.asObservable() }
    }
    func asDriver() -> Driver {
    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

    View Slide

  26. RetryWhenConfirmRetryAlert
    RxSwiftͷRetryWhenΦϖϨʔλʔΛ֦ுͯ͠೚ҙͷΤϥʔͷ৔߹
    ͷΈɺUIAlertͰϦτϥΠ͢ΔॲཧΛΦϖϨʔλʔԽɻUIAlert΋Rx
    ͢Δ͜ͱͰɺϘλϯԡԼͷϢʔβΛૢ࡞ΛετϦʔϜʹγʔϜϨ
    εʹ݁߹ɻ

    View Slide

  27. RetryWhenConfirmRetryAlert
    extension ObservableType {
    func retryWhenConfirmRetryAlert(wireframe: Wireframe) -> Observable {
    return retryWhen { (errors: Observable) -> Observable in
    return errors.flatMapWithIndex { (error, count) -> Observable 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 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 in
    return Observable.error(Error.Ignore)
    }
    }
    }
    }
    }
    }

    View Slide

  28. ɹ
    ɹ
    Realmத৺ઃܭ

    View Slide

  29. class WorkingHistoriesViewModel {
    let responseSuccessed: Driver
    init(input: (companyName: Driver, position: Driver, saveTaps: Driver),
    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 () }
    }
    }

    View Slide

  30. View Slide

  31. View Slide

  32. View Slide

  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)

    View Slide

  34. Conclusion
    • MVVMΞʔΩςΫνϟઃܭͷ۷ΓԼ͛ͯɺ۩ମతͳ࣮૷ྫͱ
    RxSwiftͷಛੑΛ׆͔ͨ͠ετϦʔϜͷهड़Λ঺հɻ
    • MVVMΞʔΩςΫνϟͰ೰Έ͕ͪͳɺΞϓϦέʔγϣϯશମʹ
    ԣஅతʹӨڹ͢Δঢ়ଶมԽʹରͯ͠ɺRealmΛ׆༻͢Δํ๏Λ
    ঺հɻ
    • ৄࡉ͸ҎલొஃࢿྉʮRealm Centered DesignʯΛࢀߟɻ

    View Slide