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

Practical application of ReactorKit

Practical application of ReactorKit

ReactorKit Meetup Japan 18/06/28

日本語: https://speakerdeck.com/yusuga/reactorkitwoshi-zhan-tou-ru-sitemite

Yu Sugawara

June 28, 2018
Tweet

More Decks by Yu Sugawara

Other Decks in Programming

Transcript

  1. • Picos LLC. • Sapporo • 3 members: iOS Engineer

    / Web Engineer / Web Designer • Currently looking for new projects!
  2. Do you know the proper implementation of ___ ? •

    ___ → MVC, MVVM, MVP, Clean Architecture, VIPER • Cost of learning • In-house usage incurs additional cost of learning
  3. Lightweight architecture • Only two protocols. 1. View protocol (StoryboardView

    protocol) 2. Reactor protocol • The less pieces, the easier to understand
  4. Lightweight architecture Layer Purpose View Raise an action, reflect state

    on the display. Reactor Update state by an action. Service Business logic. For example, API request, update DB, etc.
  5. Lightweight architecture Layer In order to... View Conform to View

    protocol. Reactor Conform to Reactor protocol. Service No protocols in place but can be implemented.
  6. Lightweight architecture • Depending on your project... • Too simple

    architecture might make a project even more complex • In our experience, it was just right
  7. Examples • Counter • The most simple and basic example

    of ReactorKit • GitHubSearch • A simple implementation of asynchronous process which provides a GitHub repository search
  8. Examples • RxTodo • How to implement TableView tasks in

    a service layer • Cleverbot • How to implement API Request in a service layer • Drrrible • A practical example available in the App Store
  9. Articles in Japanese • ReactorKit(Flux + Reactive Programming)ΛֶͿ1 ೖ໳ฤ •

    ReactorKit(Flux + Reactive Programming)ΛֶͿ2 جૅฤ • ReactorKit(Flux + Reactive Programming)ΛֶͿ3 ࣮ફฤ • Other: iOSΞϓϦͷΞʔΩςΫνϟʹ͍ͭͯߟ͑Δ • Comparison with MVC2, MVVM and ReactorKit
  10. Some troubles that we encountered • On a 1 1/2

    month project... • Some problems were difficult to understand without experiencing them firsthand
  11. ! Q #1: Who controls screen transitions? • For example

    with MVVM... • ViewModel controls screen transitions • Used for loosely coupling Views • kicksterter/ios-oss is an example which uses View (UIViewController) for screen transitions
  12. ! Q #1: Who controls screen transitions? • ! A.

    View layer • You can NOT import UIKit with Reactor
  13. RxTodo - TaskListViewController self.addButtonItem.rx.tap .map(reactor.reactorForCreatingTask) .subscribe(onNext: { [weak self] reactor

    in guard let `self` = self else { return } let viewController = TaskEditViewController(reactor: reactor) let navigationController = UINavigationController(rootViewController: viewController) self.present(navigationController, animated: true, completion: nil) }) .disposed(by: self.disposeBag)
  14. ! Q #2: Who controls Alerts? • Usually View does,

    but... let alert = AlertService(provider: reactor.provider) let alertActions: [TaskEditViewCancelAlertAction] = [.leave, .stay] self.cancelButtonItem.rx.tap .flatMap { alert.show( title: "Really?", message: "All changes will be lost", preferredStyle: .alert, actions: alertActions) } .filter { $0 == .leave } .map { _ in Reactor.Action.cancel } .bind(to: reactor.action) .disposed(by: self.disposeBag)
  15. RxTodo - TaskEditViewController final class TaskEditViewController: BaseViewController, View { func

    bind(reactor: TaskEditViewReactor) { self.cancelButtonItem.rx.tap .map { Reactor.Action.cancel } .bind(to: reactor.action) .disposed(by: self.disposeBag) } }
  16. RxTodo - TaskEditViewReactor case .cancel: if !self.currentState.shouldConfirmCancel { return .just(.dismiss)

    // no need to confirm } let alertActions: [TaskEditViewCancelAlertAction] = [.leave, .stay] return self.provider.alertService .show( title: "Really?", message: "All changes will be lost", preferredStyle: .alert, actions: alertActions ) .flatMap { alertAction -> Observable<Mutation> in switch alertAction { case .leave: return .just(.dismiss) case .stay: return .empty() } }
  17. This makes the Action of Alert type-safe case .cancel: if

    !self.currentState.shouldConfirmCancel { return .just(.dismiss) // no need to confirm } let alertActions: [TaskEditViewCancelAlertAction] = [.leave, .stay] return self.provider.alertService .show( title: "Really?", message: "All changes will be lost", preferredStyle: .alert, actions: alertActions ) .flatMap { alertAction -> Observable<Mutation> in switch alertAction { case .leave: return .just(.dismiss) case .stay: return .empty() } }
  18. Controlling Alerts with the Service layer • There are arguments

    for and against • Further details are available at ReactorKitΛֶͿ3 ࣮ફฤ • In my opinion, user operations are part of the action stream • Another example: login screens
  19. Drrrible - AuthService final class AuthService: AuthServiceType { func authorize()

    -> Observable<Void> { // ...... let safariViewController = SFSafariViewController(url: url) let navigationController = UINavigationController(rootViewController: safariViewController) navigationController.isNavigationBarHidden = true self.navigator.present(navigationController) self.currentViewController = navigationController return self.callbackSubject .flatMap(self.accessToken) .do(onNext: { [weak self] accessToken in try self?.saveAccessToken(accessToken) self?.currentAccessToken = accessToken }) .map { _ in } } }
  20. Drrrible - AuthService final class AuthService: AuthServiceType { func callback(code:

    String) { self.callbackSubject.onNext(code) self.currentViewController?.dismiss(animated: true, completion: nil) self.currentViewController = nil } }
  21. Drrrible - LoginViewReactor final class LoginViewReactor: Reactor, ServiceContainer { func

    mutate(action: Action) -> Observable<Mutation> { switch action { case .login: let setLoading: Observable<Mutation> = .just(Mutation.setLoading(true)) let setLoggedIn: Observable<Mutation> = self.authService.authorize() .flatMap { self.userService.fetchMe() } .map { true } .catchErrorJustReturn(false) .map(Mutation.setLoggedIn) return setLoading.concat(setLoggedIn) } } }
  22. Realm's Notification is excellent let results = realm.objects(Person.self).filter("age > 5")

    notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in guard let tableView = self?.tableView else { return } switch changes { case .initial: tableView.reloadData() case .update(_, let deletions, let insertions, let modifications): tableView.beginUpdates() tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .automatic) tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic) tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic) tableView.endUpdates() case .error(let error): fatalError("\(error)") } } https://realm.io/docs/swift/latest/#notifications
  23. ! Q #3: How to use in combination with Realm?

    • ! A. As a global event of a Service layer
  24. Cleverbot - MessageCellReactor final class MessageCellReactor: Reactor { typealias Action

    = NoAction struct State { var message: String? } let initialState: State init(message: Message) { self.initialState = State(message: message.text) } }
  25. ! Q #4: Do you need Reactor for NoAction? •

    ! A. Not necessary, but there are some benefit
  26. ! Q #4: Do you need Reactor for NoAction? •

    Design Goal (from Official Doc.) • Start Small: ReactorKit doesn't require the whole application to follow a single architecture. ReactorKit can be > adopted partially, for one or more specific views. You don't need to rewrite everything to use ReactorKit on your existing project. ReactorKit - README.md#design-goal
  27. Reason that I use Reactor with NoAction • Want a

    UI component to be reactive • e.g. the stream of a global event • Obey the design concept; View : Reacter ʹ 1 : 1 • Conversion over configuration works well
  28. ! Q #5: Who manages UITableViewCell's Reactor ? • For

    example, the composition of UITableViewController's Reactor and UITableViewCells' Reactors • UITableViewCells' Reactors dynamically increase and decrease Cleverbot
  29. ! Q #5: Who manages UITableViewCell's Reactor ? ! A.

    UITableViewController's Reactor Cleverbot
  30. Cleverbot - ChatViewSection struct ChatViewSection { var items: [ChatViewSectionItem] }

    enum ChatViewSectionItem { case incomingMessage(MessageCellReactor) case outgoingMessage(MessageCellReactor) }
  31. Cleverbot - ChatViewReactor final class ChatViewReactor: Reactor { struct State

    { var sections: [ChatViewSection] = [ChatViewSection(items: [])] var cleverbotState: String? = nil } }
  32. Drrrible (0.2.0) - DI import Swinject import SwinjectAutoregistration let DI

    = Container().then { $0.register(Networking<DribbbleAPI>.self) { _ in Networking(plugins: [AuthPlugin()]) } .inObjectScope(.container) $0.autoregister(AuthServiceType.self, initializer: AuthService.init) .inObjectScope(.container) $0.autoregister(UserServiceType.self, initializer: UserService.init) .inObjectScope(.container) $0.autoregister(ShotServiceType.self, initializer: ShotService.init) .inObjectScope(.container) $0.autoregister(AppStoreServiceType.self, initializer: AppStoreService.init) .inObjectScope(.container) }
  33. Drrrible (0.2.0) - ServiceContainer protocol ServiceContainer {} extension ServiceContainer {

    var networking: Networking<DribbbleAPI> { return DI.resolve(Networking<DribbbleAPI>.self)! } var authService: AuthServiceType { return DI.resolve(AuthServiceType.self)! } var userService: UserServiceType { return DI.resolve(UserServiceType.self)! } var shotService: ShotServiceType { return DI.resolve(ShotServiceType.self)! } var appStoreService: AppStoreServiceType { return DI.resolve(AppStoreServiceType.self)! } }
  34. SwinjectStoryboard (Our experience) extension SwinjectStoryboard { @objc class func setup()

    { defaultContainer.register(AppRootViewReactor.self) { _ in AppRootViewReactor() } defaultContainer.storyboardInitCompleted(AppRootViewController.self) { $1.reactor = $0.resolve(AppRootViewReactor.self) } } }
  35. I'm still having troubles with DI • The latest Drrrible

    does not have Swinject • Remove Swinject @devxoul committed on 16 Aug 2017 • I don't know why Swinject was removed, so we ask devxoul later • Perhaps planning to introduce devxoul/Pure? • Pure DI is Dependency Injection without a DI Container
  36. Future tasks • Difficult to design 'transform()' • Much easier

    to trigger 'refresh' in 'viewWillAppear()' • For more details see ReactorKitΛֶͿ3 ࣮ફฤ • I'm worried about performance of ReactorKit (and RxSwift) with larger projects
  37. Conclusion • Conversion over configuration makes development easy • Conforming

    to Protocol allows for flexible desgin • Unidirectional streams are easier to construct • Less code required for architecture