Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

ɹ ɹ ViewModelͷΠχγϟϥΠζ

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

ɹ ɹ ViewModelͷೖྗਫ਼ࠪ

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

ิ଍ 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

Slide 18

Slide 18 text

ิ଍ 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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

ɹ ɹ Realmத৺ઃܭ

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

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)

Slide 34

Slide 34 text

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