Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Yu Sugawara @yusuga_

Slide 3

Slide 3 text

• Picos LLC. • Sapporo • 3 members: iOS Engineer / Web Engineer / Web Designer • Currently looking for new projects!

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Practical application of ReactorKit

Slide 6

Slide 6 text

Reason for adoption

Slide 7

Slide 7 text

Do you know the proper implementation of ___ ?

Slide 8

Slide 8 text

Do you know the proper implementation of ___ ? • ___ → MVC, MVVM, MVP, Clean Architecture, VIPER • Cost of learning • In-house usage incurs additional cost of learning

Slide 9

Slide 9 text

It is difficult to properly implement the architecture

Slide 10

Slide 10 text

• ReactorKit is a framework for a reactive and unidirectional Swift application architecture.

Slide 11

Slide 11 text

Lightweight architecture

Slide 12

Slide 12 text

Lightweight architecture • Only two protocols. 1. View protocol (StoryboardView protocol) 2. Reactor protocol • The less pieces, the easier to understand

Slide 13

Slide 13 text

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.

Slide 14

Slide 14 text

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.

Slide 15

Slide 15 text

Lightweight architecture • Depending on your project... • Too simple architecture might make a project even more complex • In our experience, it was just right

Slide 16

Slide 16 text

Examples • Counter • The most simple and basic example of ReactorKit • GitHubSearch • A simple implementation of asynchronous process which provides a GitHub repository search

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Some troubles that we encountered

Slide 20

Slide 20 text

Some troubles that we encountered • On a 1 1/2 month project... • Some problems were difficult to understand without experiencing them firsthand

Slide 21

Slide 21 text

! Q #1: Who controls screen transitions?

Slide 22

Slide 22 text

! 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

Slide 23

Slide 23 text

! Q #1: Who controls screen transitions? • ! A. View layer • You can NOT import UIKit with Reactor

Slide 24

Slide 24 text

RxTodo

Slide 25

Slide 25 text

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)

Slide 26

Slide 26 text

! Q #2: Who controls Alerts?

Slide 27

Slide 27 text

RxTodo

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

! Q #2: Who controls Alerts? • ! A. Service layer

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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 in switch alertAction { case .leave: return .just(.dismiss) case .stay: return .empty() } }

Slide 32

Slide 32 text

AlertService • RxTodo/Sources/Services/AlertService is amazing • This makes the Action of Alert type-safe.

Slide 33

Slide 33 text

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 in switch alertAction { case .leave: return .just(.dismiss) case .stay: return .empty() } }

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Drrrible

Slide 36

Slide 36 text

Drrrible - AuthService final class AuthService: AuthServiceType { func authorize() -> Observable { // ...... 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 } } }

Slide 37

Slide 37 text

Drrrible - AuthService final class AuthService: AuthServiceType { fileprivate let callbackSubject = PublishSubject() }

Slide 38

Slide 38 text

Drrrible - AuthService final class AuthService: AuthServiceType { func callback(code: String) { self.callbackSubject.onNext(code) self.currentViewController?.dismiss(animated: true, completion: nil) self.currentViewController = nil } }

Slide 39

Slide 39 text

Drrrible - LoginViewReactor final class LoginViewReactor: Reactor, ServiceContainer { func mutate(action: Action) -> Observable { switch action { case .login: let setLoading: Observable = .just(Mutation.setLoading(true)) let setLoggedIn: Observable = self.authService.authorize() .flatMap { self.userService.fetchMe() } .map { true } .catchErrorJustReturn(false) .map(Mutation.setLoggedIn) return setLoading.concat(setLoggedIn) } } }

Slide 40

Slide 40 text

! Q #3: How to use in combination with Realm?

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

! Q #3: How to use in combination with Realm? • ! A. As a global event of a Service layer

Slide 43

Slide 43 text

Global Event

Slide 44

Slide 44 text

RxTodo - TaskService

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

ReactorKit - Reactor public struct NoAction {}

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

! 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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

! Q #5: Who manages UITableViewCell's Reactor ?

Slide 52

Slide 52 text

! 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

Slide 53

Slide 53 text

! Q #5: Who manages UITableViewCell's Reactor ? ! A. UITableViewController's Reactor Cleverbot

Slide 54

Slide 54 text

Cleverbot - ChatViewSection struct ChatViewSection { var items: [ChatViewSectionItem] } enum ChatViewSectionItem { case incomingMessage(MessageCellReactor) case outgoingMessage(MessageCellReactor) }

Slide 55

Slide 55 text

Cleverbot - ChatViewReactor final class ChatViewReactor: Reactor { struct State { var sections: [ChatViewSection] = [ChatViewSection(items: [])] var cleverbotState: String? = nil } }

Slide 56

Slide 56 text

6. Dependency Injection

Slide 57

Slide 57 text

Drrrible (0.2.0) - DI import Swinject import SwinjectAutoregistration let DI = Container().then { $0.register(Networking.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) }

Slide 58

Slide 58 text

Drrrible (0.2.0) - ServiceContainer protocol ServiceContainer {} extension ServiceContainer { var networking: Networking { return DI.resolve(Networking.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)! } }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

Conclusion

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Thank you!ɹɹɹ ɹ