Share
Embed
Practical application of ReactorKit
ReactorKit Meetup Japan 18/06/28
日本語: https://speakerdeck.com/yusuga/reactorkitwoshi-zhan-tou-ru-sitemite
Yu Sugawara
June 28, 2018
Transcript
64 slides
-
• Picos LLC. • Sapporo • 3 members: iOS Engineer
/ Web Engineer / Web Designer • Currently looking for new projects! -
Do you know the proper implementation of ___ ? •
___ → MVC, MVVM, MVP, Clean Architecture, VIPER • Cost of learning • In-house usage incurs additional cost of learning -
• ReactorKit is a framework for a reactive and unidirectional
Swift application architecture. -
Lightweight architecture • Only two protocols. 1. View protocol (StoryboardView
protocol) 2. Reactor protocol • The less pieces, the easier to understand -
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. -
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. -
Lightweight architecture • Depending on your project... • Too simple
architecture might make a project even more complex • In our experience, it was just right -
Examples • Counter • The most simple and basic example
of ReactorKit • GitHubSearch • A simple implementation of asynchronous process which provides a GitHub repository search -
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 -
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 -
Some troubles that we encountered • On a 1 1/2
month project... • Some problems were difficult to understand without experiencing them firsthand -
! 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 -
! Q #1: Who controls screen transitions? • ! A.
View layer • You can NOT import UIKit with Reactor -
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) -
! 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) -
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) } } -
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() } } -
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() } } -
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 -
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 } } } -
Drrrible - AuthService final class AuthService: AuthServiceType { fileprivate let
callbackSubject = PublishSubject<String>() } -
Drrrible - AuthService final class AuthService: AuthServiceType { func callback(code:
String) { self.callbackSubject.onNext(code) self.currentViewController?.dismiss(animated: true, completion: nil) self.currentViewController = nil } } -
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) } } } -
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 -
! Q #3: How to use in combination with Realm?
• ! A. As a global event of a Service layer -
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) } } -
! Q #4: Do you need Reactor for NoAction? •
! A. Not necessary, but there are some benefit -
! 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 -
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 -
! 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 -
! Q #5: Who manages UITableViewCell's Reactor ? ! A.
UITableViewController's Reactor Cleverbot -
Cleverbot - ChatViewSection struct ChatViewSection { var items: [ChatViewSectionItem] }
enum ChatViewSectionItem { case incomingMessage(MessageCellReactor) case outgoingMessage(MessageCellReactor) } -
Cleverbot - ChatViewReactor final class ChatViewReactor: Reactor { struct State
{ var sections: [ChatViewSection] = [ChatViewSection(items: [])] var cleverbotState: String? = nil } } -
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) } -
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)! } } -
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) } } } -
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 -
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 -
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