Practical application of ReactorKit

Practical application of ReactorKit

ReactorKit Meetup Japan 18/06/28

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

Df8bc8c531e2c5c89c1a007db1cf79a3?s=128

Yu Sugawara

June 28, 2018
Tweet

Transcript

  1. Practical application of ReactorKit ReactorKit Meetup Japan 18/06/28

  2. Yu Sugawara @yusuga_

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

    / Web Engineer / Web Designer • Currently looking for new projects!
  4. None
  5. Practical application of ReactorKit

  6. Reason for adoption

  7. Do you know the proper implementation of ___ ?

  8. Do you know the proper implementation of ___ ? •

    ___ → MVC, MVVM, MVP, Clean Architecture, VIPER • Cost of learning • In-house usage incurs additional cost of learning
  9. It is difficult to properly implement the architecture

  10. • ReactorKit is a framework for a reactive and unidirectional

    Swift application architecture.
  11. Lightweight architecture

  12. Lightweight architecture • Only two protocols. 1. View protocol (StoryboardView

    protocol) 2. Reactor protocol • The less pieces, the easier to understand
  13. 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.
  14. 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.
  15. Lightweight architecture • Depending on your project... • Too simple

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

    of ReactorKit • GitHubSearch • A simple implementation of asynchronous process which provides a GitHub repository search
  17. 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
  18. 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
  19. Some troubles that we encountered

  20. Some troubles that we encountered • On a 1 1/2

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

  22. ! 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
  23. ! Q #1: Who controls screen transitions? • ! A.

    View layer • You can NOT import UIKit with Reactor
  24. RxTodo

  25. 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)
  26. ! Q #2: Who controls Alerts?

  27. RxTodo

  28. ! 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)
  29. ! Q #2: Who controls Alerts? • ! A. Service

    layer
  30. 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) } }
  31. 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() } }
  32. AlertService • RxTodo/Sources/Services/AlertService is amazing • This makes the Action

    of Alert type-safe.
  33. 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() } }
  34. 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
  35. Drrrible

  36. 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 } } }
  37. Drrrible - AuthService final class AuthService: AuthServiceType { fileprivate let

    callbackSubject = PublishSubject<String>() }
  38. Drrrible - AuthService final class AuthService: AuthServiceType { func callback(code:

    String) { self.callbackSubject.onNext(code) self.currentViewController?.dismiss(animated: true, completion: nil) self.currentViewController = nil } }
  39. 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) } } }
  40. ! Q #3: How to use in combination with Realm?

  41. 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
  42. ! Q #3: How to use in combination with Realm?

    • ! A. As a global event of a Service layer
  43. Global Event

  44. RxTodo - TaskService

  45. ! Q #4: Do you need Reactor for NoAction?

  46. ReactorKit - Reactor public struct NoAction {}

  47. 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) } }
  48. ! Q #4: Do you need Reactor for NoAction? •

    ! A. Not necessary, but there are some benefit
  49. ! 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
  50. 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
  51. ! Q #5: Who manages UITableViewCell's Reactor ?

  52. ! 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
  53. ! Q #5: Who manages UITableViewCell's Reactor ? ! A.

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

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

    { var sections: [ChatViewSection] = [ChatViewSection(items: [])] var cleverbotState: String? = nil } }
  56. 6. Dependency Injection

  57. 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) }
  58. 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)! } }
  59. 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) } } }
  60. 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
  61. 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
  62. Conclusion

  63. 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
  64. Thank you!ɹɹɹ ɹ