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

iOS Architecture: A State Container based approach

iOS Architecture: A State Container based approach

Luis Recuenco

January 10, 2018
Tweet

More Decks by Luis Recuenco

Other Decks in Programming

Transcript

  1. 1

  2. Books and movies are like apples and oranges. They both

    are fruit, but taste completely different — Stephen King
  3. View Controller has too many responsibilities • View lifecycle •

    Business logic • Data transformation • Network requests • Delegate and data source for UI components • And so on and so forth...
  4. MVP

  5. class ProcessListViewModel { private(set) var loading: Bool private(set) var processes:

    [Process] private(set) var error: Error func fetchProcesses() { loading = true service.loadProcesses { processes, error in if error { self.error = error } else { self.processes = processes } } } }
  6. class ProcessListViewModel { private(set) var loading: Bool private(set) var processes:

    [Process] private(set) var error: Error func fetchProcesses() { loading = true service.loadProcesses { processes, error in if error { self.error = error } else { self.processes = processes } } } }
  7. class ProcessListViewModel { private(set) var loading: Bool private(set) var processes:

    [Process] private(set) var error: Error func fetchProcesses() { loading = true service.loadProcesses { processes, error in if error { self.error = error } else { self.processes = processes } } } }
  8. class ProcessListViewModel { private(set) var loading: Bool private(set) var processes:

    [Process] private(set) var error: Error func fetchProcesses() { loading = true service.loadProcesses { processes, error in if error { self.error = error } else { self.processes = processes } } } }
  9. struct ProcessListViewState { let loading: Bool let processes: [ProcessViewState]? let

    error: Error? static func loading() -> ProcessListViewState { return ProcessListViewState(loading: true, processes: nil, error: nil) } static func loaded(with processes: [Process]) -> ProcessListViewState { return ProcessListViewState(loading: false, processes: processes, error: nil) } static func error(with error: Error) -> ProcessListViewState { return ProcessListViewState(loading: false, processes: nil, error: error) } }
  10. struct ProcessListViewState { let loading: Bool let processes: [ProcessViewState]? let

    error: Error? static func loading() -> ProcessListViewState { return ProcessListViewState(loading: true, processes: nil, error: nil) } static func loaded(with processes: [Process]) -> ProcessListViewState { return ProcessListViewState(loading: false, processes: processes, error: nil) } static func error(with error: Error) -> ProcessListViewState { return ProcessListViewState(loading: false, processes: nil, error: error) } }
  11. struct ProcessListViewState { let loading: Bool let processes: [ProcessViewState]? let

    error: Error? static func loading() -> ProcessListViewState { return ProcessListViewState(loading: true, processes: nil, error: nil) } static func loaded(with processes: [Process]) -> ProcessListViewState { return ProcessListViewState(loading: false, processes: processes, error: nil) } static func error(with error: Error) -> ProcessListViewState { return ProcessListViewState(loading: false, processes: nil, error: error) } }
  12. class ProcessListViewModel { private(set) var state: ProcessListViewState { didSet {

    queue.sync { // Save new state and notify subscribers } } } func fetchProcesses() { state = ProcessListViewState.loading() service.loadProcesses { processes, error in if error { state = ProcessListViewState.error(with: error) } else { state = ProcessListViewState.loaded(with: process) } } } }
  13. class ProcessListViewModel { private(set) var state: ProcessListViewState { didSet {

    queue.sync { // Save new state, log it, serialize it and notify subscribers } } } func fetchProcesses() { state = ProcessListViewState.loading() service.loadProcesses { processes, error in if error { state = ProcessListViewState.error(with: error) } else { state = ProcessListViewState.loaded(with: process) } } } }
  14. class ProcessListViewModel { private(set) var state: ProcessListViewState { didSet {

    queue.sync { // Save new state, log it, serialize it and notify subscribers } } } func fetchProcesses() { state = ProcessListViewState.loading() service.loadProcesses { processes, error in if error { state = ProcessListViewState.error(with: error) } else { state = ProcessListViewState.loaded(with: process) } } } }
  15. class ProcessListViewController { init(viewModel: ProcessListViewModel) { viewModel.subscribe(from: self) } func

    render(state: ProcessListViewState) { if let error = state.error { ... } else if state.loading { ... } else if let processes = state.processes { ... } } }
  16. class ProcessListViewController { init(viewModel: ProcessListViewModel) { viewModel.subscribe(from: self) } func

    render(state: ProcessListViewState) { if let error = state.error { ... } else if state.loading { ... } else if let processes = state.processes { ... } } }
  17. OR

  18. Pattern matching service.doNetworkRequest { result in switch result { case

    .failure(let error): handleError(with: error) case .success(let value): renderScreen(with: value) } }
  19. enum ProcessListViewState { case loading case loaded([ProcessViewState]) case error(Error) }

    class ProcessListViewController { func render(state: ProcessListViewState) { switch state { case .loading: renderLoading() case .loaded(let processes): render(processes) case .error(let error): render(error) } } }
  20. enum ProcessListViewState { case loading case loaded([ProcessViewState]) case error(Error) }

    class ProcessListViewController { func render(state: ProcessListViewState) { switch state { case .loading: renderLoading() case .loaded(let processes): render(processes) case .error(let error): render(error) } } }
  21. enum ProcessListViewState { case loading case loaded([ProcessViewState]) case error(Error) mutating

    func toLoading() { switch self { case .loading: fatalError("Wrong state transition") case .loaded, .idle, .error: self = .loading } } }
  22. enum ProcessListViewState { case loading case loaded([ProcessViewState]) case error(Error) mutating

    func toLoading() { switch self { case .loading: fatalError("Wrong state transition") case .loaded, .idle, .error: self = .loading } } }
  23. ==

  24. ===

  25. Queries extension ProcessStoreState { var processes: [Process]? { switch self

    { case .idle, .loading, .error: return nil case .loaded(let processes): return processes } } var error: Error? { switch self { case .idle, .loaded, .loading: return nil case .error(let error): return error } } }
  26. Commands extension ProcessStoreState { mutating func enterProcess(id: String) { //

    create modifiedProcesses... self = .loaded(modifiedProcesses) } mutating func toLoading() { // Check correct state transition if needed self = .loading } }
  27. enum ProcessListViewState { case loading case loaded([ProcessViewState]) case error(Error) init(domainState:

    ProcessStoreState) { switch domainState { case .idle, .loading: self = .loading case .loaded(let processes): self = .loaded(processes.map { ... }) case .error: self = .error(error.map { ... }) } } }
  28. class ProcessListViewModel { private let store: ProcessStore private(set) var state:

    ProcessListViewState { didSet { print("New state: \(viewState)") } } init(store: ProcessStore) { self.store = store subscribe(to: store) { newDomainState in state = ProcessListViewState(domainState: newDomainState) } } func fetchProcesses() { store.fetchProcesses() } }
  29. 2

  30. class Store<State: StoreState> { ... private lazy var stateTransactionQueue =

    DispatchQueue(label: "com.jobandtalent.\(type(of: self)).StateTransactionQueue") var state: State { didSet(oldState) { if #available(iOS 10.0, *) { dispatchPrecondition(condition: .onQueue(stateTransactionQueue)) } stateDidChange(oldState: oldState, newState: state) } } ... }
  31. class Store<State: StoreState> { ... private lazy var stateTransactionQueue =

    DispatchQueue(label: "com.jobandtalent.\(type(of: self)).StateTransactionQueue") var state: State { didSet(oldState) { if #available(iOS 10.0, *) { dispatchPrecondition(condition: .onQueue(stateTransactionQueue)) } stateDidChange(oldState: oldState, newState: state) } } ... }
  32. class Store<State: StoreState> { ... private lazy var stateTransactionQueue =

    DispatchQueue(label: "com.jobandtalent.\(type(of: self)).StateTransactionQueue") var state: State { didSet(oldState) { if #available(iOS 10.0, *) { dispatchPrecondition(condition: .onQueue(stateTransactionQueue)) } stateDidChange(oldState: oldState, newState: state) } } ... }
  33. class Store<State: StoreState> { ... func stateDidChange(oldState: State, newState: State)

    { guard oldState != newState else { print("Skip forwarding same state: \(newState)") return } print("State change: \(newState)") subscriptions.allObjects.forEach { $0.fire(state) } } }
  34. class Store<State: StoreState> { ... func stateDidChange(oldState: State, newState: State)

    { guard oldState != newState else { print("Skip forwarding same state: \(newState)") return } print("State change: \(newState)") subscriptions.allObjects.forEach { $0.fire(state) } } }
  35. class Store<State: StoreState> { ... func stateDidChange(oldState: State, newState: State)

    { guard oldState != newState else { print("Skip forwarding same state: \(newState)") return } print("State change: \(newState)") subscriptions.allObjects.forEach { $0.fire(state) } } }
  36. class Store<State: StoreState> { ... func stateDidChange(oldState: State, newState: State)

    { guard oldState != newState else { print("Skip forwarding same state: \(newState)") return } print("State change: \(newState)") subscriptions.allObjects.forEach { $0.fire(state) } } }
  37. class StateSubscription<State: StoreState> { private(set) var block: ((State) -> Void)?

    func fire(_ state: State) { block?(state) } func stop() { block = nil } deinit { stop() } }
  38. class StateSubscription<State: StoreState> { private(set) var block: ((State) -> Void)?

    func fire(_ state: State) { block?(state) } func stop() { block = nil } deinit { stop() } }
  39. class StateSubscription<State: StoreState> { private(set) var block: ((State) -> Void)?

    func fire(_ state: State) { block?(state) } func stop() { block = nil } deinit { stop() } }
  40. class Store<State: StoreState> { private var subscriptions = NSHashTable<StateSubscription<State>>.weakObjects() func

    subscribe(_ block: @escaping (State) -> Void) -> StateSubscription<State> { let subscription = StateSubscription(block) subscriptions.add(subscription) subscription.fire(state) return subscription } }
  41. class Store<State: StoreState> { private var subscriptions = NSHashTable<StateSubscription<State>>.weakObjects() func

    subscribe(_ block: @escaping (State) -> Void) -> StateSubscription<State> { let subscription = StateSubscription(block) subscriptions.add(subscription) subscription.fire(state) return subscription } }
  42. class Store<State: StoreState> { private var subscriptions = NSHashTable<StateSubscription<State>>.weakObjects() func

    subscribe(_ block: @escaping (State) -> Void) -> StateSubscription<State> { let subscription = StateSubscription(block) subscriptions.add(subscription) subscription.fire(state) return subscription } }
  43. class Store<State: StoreState> { private var subscriptions = NSHashTable<StateSubscription<State>>.weakObjects() func

    subscribe(_ block: @escaping (State) -> Void) -> StateSubscription<State> { let subscription = StateSubscription(block) subscriptions.add(subscription) subscription.fire(state) return subscription } }
  44. class Store<State: StoreState> { private var subscriptions = NSHashTable<StateSubscription<State>>.weakObjects() func

    subscribe(_ block: @escaping (State) -> Void) -> StateSubscription<State> { let subscription = StateSubscription(block) subscriptions.add(subscription) subscription.fire(state) return subscription } }
  45. class Store<State: StoreState> { private var subscriptions = NSHashTable<StateSubscription<State>>.weakObjects() func

    subscribe(_ block: @escaping (State) -> Void) -> StateSubscription<State> { let subscription = StateSubscription(block) subscriptions.add(subscription) subscription.fire(state) return subscription } }
  46. class ViewModel<State: ViewState>: Store<State> { private var views = Set<AnyStatefulView<State>>()

    override var state: State { didSet(oldState) { views.forEach { stateDidChange(oldState: oldState, newState: state, view: $0) } } } }
  47. class ViewModel<State: ViewState>: Store<State> { private var views = Set<AnyStatefulView<State>>()

    override var state: State { didSet(oldState) { views.forEach { stateDidChange(oldState: oldState, newState: state, view: $0) } } } }
  48. class ViewModel<State: ViewState>: Store<State> { private func stateDidChange(oldState: State, newState:

    State, view: AnyStatefulView<State>) { switch view.renderPolicy { case .possible: if newState == oldState { return } DispatchQueue.main.async { view.render(state: newState) } case .notPossible(let renderError): // Handle error } } }
  49. class ViewModel<State: ViewState>: Store<State> { private func stateDidChange(oldState: State, newState:

    State, view: AnyStatefulView<State>) { switch view.renderPolicy { case .possible: if newState == oldState { return } DispatchQueue.main.async { view.render(state: newState) } case .notPossible(let renderError): // Handle error } } }
  50. class ViewModel<State: ViewState>: Store<State> { private func stateDidChange(oldState: State, newState:

    State, view: AnyStatefulView<State>) { switch view.renderPolicy { case .possible: if newState == oldState { return } DispatchQueue.main.async { view.render(state: newState) } case .notPossible(let renderError): // Handle error } } }
  51. class ViewModel<State: ViewState>: Store<State> { private func stateDidChange(oldState: State, newState:

    State, view: AnyStatefulView<State>) { switch view.renderPolicy { case .possible: if newState == oldState { return } DispatchQueue.main.async { view.render(state: newState) } case .notPossible(let renderError): // Handle error } } }
  52. class ViewModel<State: ViewState>: Store<State> { private func stateDidChange(oldState: State, newState:

    State, view: AnyStatefulView<State>) { switch view.renderPolicy { case .possible: if newState == oldState { return } DispatchQueue.main.async { view.render(state: newState) } case .notPossible(let renderError): // Handle error } } }
  53. class ViewModel<State: ViewState>: Store<State> { private func stateDidChange(oldState: State, newState:

    State, view: AnyStatefulView<State>) { switch view.renderPolicy { case .possible: if newState == oldState { return } DispatchQueue.main.async { view.render(state: newState) } case .notPossible(let renderError): // Handle error } } }
  54. 3

  55. Problems • Not being able to subscribe to specific parts

    of the state. • Not persistent data structures.
  56. Problems • Not being able to subscribe to specific parts

    of the state. • Not persistent data structures. • No guarantee only subclasses can change the state (no protected modifier in swift).
  57. Problems • Not being able to subscribe to specific parts

    of the state. • Not persistent data structures. • No guarantee only subclasses can change the state (no protected modifier in swift). • Mutating can be cumbersome with big trees -> leverage composition.
  58. Problems • Not being able to subscribe to specific parts

    of the state. • Not persistent data structures. • No guarantee only subclasses can change the state (no protected modifier in swift). • Mutating can be cumbersome with sum types -> leverage composition. • Stores interdependencies can lead to infinite loops.