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

Mikhail Rakhmanov - Redux in production for a s...

Avatar for CocoaHeads Berlin CocoaHeads Berlin
December 04, 2020
30

Mikhail Rakhmanov - Redux in production for a simple app

In this talk Mikhail will describe his experience working with Redux on a simple production app for Russian clothes brand named Outlaw. Also he will briefly mention how he used some of the Redux elements in bigger production apps.

Mikhail will focus on different aspects of the architecture, starting from explaining the concept in general and ending with more complex aspects as implementing services and navigation. In the end we will discuss when it does make sense to use Redux and when not and if it plays well with UIKit (not always especially with CoreData and TableViews).

Mikhail is a software developer from 2016. Worked in many Russian companies including Yandex (on Yandex Translate iOS app). Now working in Blacklane as an iOS developer and also helping several backend teams.

Video: https://youtu.be/GQOgKRfGK_s

Avatar for CocoaHeads Berlin

CocoaHeads Berlin

December 04, 2020
Tweet

Transcript

  1. Redux in iOS Main entities in Redux Key implementation aspects

    in iOS Advantages and disadvantages What is Redux?
  2. Redux in iOS Main entities in Redux Key implementation aspects

    in iOS Advantages and disadvantages What is Redux?
  3. Redux in iOS Main entities in Redux Key implementation aspects

    in iOS Advantages and disadvantages What is Redux?
  4. Redux in iOS Main entities in Redux Key implementation aspects

    in iOS Advantages and disadvantages What is Redux?
  5. What is Redux? View (Subscriber) Reducer Reducer Reducer … Action

    Store Action State New State New State Unidirectional Architecture!
  6. • One and only way to interact with the Store

    Action • Sends arbitrary data to Store • Struct or Enum
  7. • One and only way to interact with the Store

    Action • Sends arbitrary data to Store • Struct or Enum
  8. • One and only way to interact with the Store

    Action • Sends arbitrary data to Store • Struct or Enum
  9. Application State public protocol StateType {} struct AppState: StateType {

    var navigationState = NavigationState() var currentProductState = CurrentProductState() var onboardingState = OnboardingState() var historyListState = HistoryListState() var photoGalleryState = PhotoGalleryState() }
  10. • Immutable -> use structs State (continued) • Copies can

    be made too often (use copy on write if needed) • One state for the App (!)
  11. • Immutable -> use structs State (continued) • Copies can

    be made too often (use copy on write if needed) • One state for the App (!)
  12. • Immutable -> use structs State (continued) • Copies can

    be made too often (use copy on write if needed) • One state for the App (!)
  13. Application Reducer func appReducer(action: Action, state: AppState?) -> AppState {

    guard let state = state else { return AppState() } let newNavigationState = navigationReducer( action: action, navigationState: state.navigationState ) ... return AppState( navigationState: newNavigationState, currentProductState: newCurrentProductState, onboardingState: newOnboardingState, historyListState: newHistoryListState, photoGalleryState: newPhotoGalleryState ) }
  14. Application Reducer func appReducer(action: Action, state: AppState?) -> AppState {

    guard let state = state else { return AppState() } let newNavigationState = navigationReducer( action: action, navigationState: state.navigationState ) ... return AppState( navigationState: newNavigationState, currentProductState: newCurrentProductState, onboardingState: newOnboardingState, historyListState: newHistoryListState, photoGalleryState: newPhotoGalleryState ) }
  15. Screen Reducer func historyListReducer( action: Action, historyListState: HistoryListState ) ->

    HistoryListState { var newState = historyListState switch action { case let result as HistoryFetchResultAction: switch result { case .success(let entries): newState.entries = entries case .error: newState.entries = [] } } return newState }
  16. Screen Reducer func historyListReducer( action: Action, historyListState: HistoryListState ) ->

    HistoryListState { var newState = historyListState switch action { case let result as HistoryFetchResultAction: switch result { case .success(let entries): newState.entries = entries case .error: newState.entries = [] } } return newState }
  17. • Pure functions Reducers (continued) • Synchronous (!) -> no

    complex calculations • Only for changes of State
  18. • Pure functions Reducers (continued) • Synchronous (!) -> no

    complex calculations • Only for changes of State
  19. • Pure functions Reducers (continued) • Synchronous (!) -> no

    complex calculations • Only for changes of State
  20. Subscription override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) store.subscribe(self, transform:

    { subscription in subscription.select { $0.currentProductState } }) } extension ProductDetailViewController: StoreSubscriber { typealias StoreSubscriberStateType = CurrentProductState public func newState(state: CurrentProductState) { DispatchQueue.main.async { self.updateView(state: state) } } }
  21. Subscription override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) store.subscribe(self, transform:

    { subscription in subscription.select { $0.currentProductState } }) } extension ProductDetailViewController: StoreSubscriber { typealias StoreSubscriberStateType = CurrentProductState public func newState(state: CurrentProductState) { DispatchQueue.main.async { self.updateView(state: state) } } }
  22. • We cannot put them inside Reducers What about asynchronous

    tasks? • We also cannot put them inside View • Middlewares to the rescue!
  23. • We cannot put them inside Reducers What about asynchronous

    tasks? • We also cannot put them inside View • Middlewares to the rescue!
  24. • We cannot put them inside Reducers What about asynchronous

    tasks? • We also cannot put them inside View • Middlewares to the rescue!
  25. Middleware func fetchProduct(action: Action, context: MiddlewareContext<AppState>) -> Action? { guard

    let action = action as? HistoryTransitionAction else { return action } Verisium.fetchProduct(reference: reference) { result in switch result { case .success(let product): context.dispatch(ProductFetchAction.completed(product)) case .failure(_): context.dispatch(ProductFetchAction.failed) } } return action }
  26. Middleware func fetchProduct(action: Action, context: MiddlewareContext<AppState>) -> Action? { guard

    let action = action as? HistoryTransitionAction else { return action } Verisium.fetchProduct(reference: reference) { result in switch result { case .success(let product): context.dispatch(ProductFetchAction.completed(product)) case .failure(_): context.dispatch(ProductFetchAction.failed) } } return action }
  27. • Keep navigation out of State (not really good) Navigation

    • Insert it inside State (not very easy, monitor UIKit)
  28. • Keep navigation out of State (not really good) Navigation

    • Insert it inside State (not very easy, monitor UIKit)
  29. • Is a graph/tree Navigation State • Think of a

    tab bar transitions as switching a branch • Think of modal transitions as creating more levels in the tree
  30. • Is a graph/tree Navigation State • Think of a

    tab bar transitions as switching a branch • Think of modal transitions as creating more levels in the tree
  31. • Is a graph/tree Navigation State • Think of a

    tab bar transitions as switching a branch • Think of modal transitions as creating more levels in the tree
  32. • You need to keep track of all changes! Navigation

    State (continued) • Need to write custom components • Be ready to face difficulties and find adhoc solutions
  33. • You need to keep track of all changes! Navigation

    State (continued) • Need to write custom components • Be ready to face difficulties and find adhoc solutions
  34. • You need to keep track of all changes! Navigation

    State (continued) • Need to write custom components • Be ready to face difficulties and find adhoc solutions
  35. AdHoc example final class VideoPlayerViewController: AVPlayerViewController { override func viewDidDisappear(_

    animated: Bool) { super.viewDidDisappear(animated) store.dispatch(VideoPlayerCloseAction(...)) } }
  36. func navigationReducer(action: Action, navigationState: NavigationState ) -> NavigationState { var

    state = navigationState Navigation Reducer switch action { case let photoGalleryAction as PhotoGalleryAction: switch photoGalleryAction { case .open, .openMainPhoto: state.addTransition(.currentProduct, .photoGallery) case .close: state.removeLastTransition() default: break } } return state }
  37. func navigationReducer(action: Action, navigationState: NavigationState ) -> NavigationState { var

    state = navigationState Navigation Reducer switch action { case let photoGalleryAction as PhotoGalleryAction: switch photoGalleryAction { case .open, .openMainPhoto: state.addTransition(.currentProduct, .photoGallery) case .close: state.removeLastTransition() default: break } } return state }
  38. Transition Handler final class ScreenTransitionHandler: StoreSubscriber { func newState(state: NavigationState)

    { if state.lastTransition != self.lastKnownTransition { self.lastKnownTransition = state.lastTransition self.performTransition(state: state) } } func performTransition(state: NavigationState) { for (_, observer) in transitionObservers { observer.value?.newTransition(state.lastTransition) } } }
  39. Transition Handler final class ScreenTransitionHandler: StoreSubscriber { func newState(state: NavigationState)

    { if state.lastTransition != self.lastKnownTransition { self.lastKnownTransition = state.lastTransition self.performTransition(state: state) } } func performTransition(state: NavigationState) { for (_, observer) in transitionObservers { observer.value?.newTransition(state.lastTransition) } } }
  40. Coordinator final class CurrentProductCoordinator: TransitionObserver { func newTransition(transition: Transition) {

    switch transition.direction { case (.currentProduct, .videoPlayer): ... case (.currentProduct, .photoGallery): ... productController.present(...) case (.currentProduct, .safari): ... } } }
  41. • Screen composition reflected in State Navigation State (recap) •

    Transitions are instantiated via Actions • Need to observe UIKit transitions
  42. • Screen composition reflected in State Navigation State (recap) •

    Transitions are instantiated via Actions • Need to observe UIKit transitions
  43. • Screen composition reflected in State Navigation State (recap) •

    Transitions are instantiated via Actions • Need to observe UIKit transitions
  44. • Should we keep separate object for view related details?

    UI State • What if we need to animate from one state to another?
  45. • Should we keep separate object for view related details?

    UI State • What if we need to animate from one state to another?
  46. • Keep elements cached inside view Product Detail Example •

    Check if something is changed (State diff) • Update only the needed stuff
  47. • Keep elements cached inside view Product Detail Example •

    Check if something is changed (State diff) • Update only the needed stuff
  48. • Keep elements cached inside view Product Detail Example •

    Check if something is changed (State diff) • Update only the needed stuff
  49. Product Detail Example final class ProductDetailDataSource { func update( with

    state: CurrentProductState, tableView: UITableView ) { guard items.isEmpty else { updateMainPhotoIfNeeded(state: state) updateVideoPlayerState(state: state.videoPlayerState) return } items = tableViewModelFactory.createTableViewModels( state: state ) tableView.reloadData() } }
  50. Product Detail Example final class ProductDetailDataSource { func update( with

    state: CurrentProductState, tableView: UITableView ) { guard items.isEmpty else { updateMainPhotoIfNeeded(state: state) updateVideoPlayerState(state: state.videoPlayerState) return } items = tableViewModelFactory.createTableViewModels( state: state ) tableView.reloadData() } }
  51. • Everything has a State -> clear and explicit logic

    Advantages • Easy to broadcast application wide events • State is always synchronised
  52. • Everything has a State -> clear and explicit logic

    Advantages • Easy to broadcast application wide events • State is always synchronised
  53. • Everything has a State -> clear and explicit logic

    Advantages • Easy to broadcast application wide events • State is always synchronised
  54. • Navigation can be complex Disadvantages • One store per

    app is not convenient • Frequent updates can be tricky • Core Data and NSFetchedResultsController not welcomed
  55. • Navigation can be complex Disadvantages • One store per

    app is not convenient • Frequent updates can be tricky • Core Data and NSFetchedResultsController not welcomed
  56. • Navigation can be complex Disadvantages • One store per

    app is not convenient • Frequent updates can be tricky • Core Data and NSFetchedResultsController not welcomed
  57. • Navigation can be complex Disadvantages • One store per

    app is not convenient • Frequent updates can be tricky • Core Data and NSFetchedResultsController not welcomed
  58. • Make state based screens Key takeaways • Experiment with

    Redux-like architectures, but be careful with complex projects • You can use store only for part of the business logic
  59. • Make state based screens Key takeaways • You can

    use store only for part of the business logic • Experiment with Redux-like architectures, but be careful with complex projects
  60. • Make state based screens Key takeaways • Experiment with

    Redux-like architectures, but be careful with complex projects • You can use store only for part of the business logic