iOS: Solved

Please, hold your applause.
  1. Unidirectional Data Flow
  1. Unidirectional Data Flow Redux

Redux on iOS gots some problems

Redux Problems

  • Async work

Redux Problems

  • Async work
  • User actions vs data actions

Redux Problems

  • Async work
  • User actions vs data actions
  • Routing with UIKit
  1. Unidirectional Data Flow Redux
  1. Unidirectional Data Flow Redux
  2. ???

What's a view controller?

  1. Renders view
  2. Handles user input
  1. Renders its view
  2. Handles its user input

struct MyViewModel {

    let viewState: String

    // ...

}


protocol MyHandler: class {

    func doSomething()

    // ...

}


final class MyViewController: UIViewController {

    weak var handler: MyHandler!


    func inject(handler handler: MyHandler) {

        self.handler = handler

    }


    func render(viewModel: MyViewModel) {

        // ...

    }


    @IBAction func doSomething(sender: AnyObject) {

        handler.doSomething()

    }

}

Nothing else?!?

No

  • Logic

No

  • Logic
  • Dependencies

No

  • Logic
  • Dependencies
  • Navigation

No

  • Logic
  • Dependencies
  • Navigation
  • Redux
  1. Unidirectional Data Flow Redux
  2. ???
  1. Unidirectional Data Flow Redux
  2. App Coordinators

App Coordinators

I didn't invent this

App Coordinators

  • Instantiate view controllers

App Coordinators

  • Instantiate view controllers
  • Handle user actions

App Coordinators

  • Instantiate view controllers
  • Handle user actions
  • Perform navigation
  1. Unidirectional Data Flow Redux
  2. ???
  1. Unidirectional Data Flow Redux
  2. App Coordinators

Redux & App Coordinators

App Coordinators & Redux

introducing

Cordux

App coordinators instantiate view controllers

App coordinators instantiate view controllers

& subscribe them to the store

App coordinators handle user actions

App coordinators handle user actions

& dispatch data actions to the store

App coordinators perform navigation

App coordinators perform navigation

via the app state's route

public protocol Coordinator: class {

    var route: Route { get set }

    var rootViewController: UIViewController { get }

    func start()

}

init(allYourDependencies: ...)

Media App Example

Route

Scene Coordinators

Tab Bar Scenes

Navigation Controller Scenes

Framework Help

public protocol SceneCoordinator: Coordinator {

    var scenePrefix: String { get }

    var currentScene: Coordinator? { get }

    func changeScene(route: Route)

    func sceneRoute(route: Route) -> Route

}


public extension SceneCoordinator {

    public var route: Route {

        get {

            // ...

        }

        set {

            // ...

        }

    }


    // ...

}

Framework Help

public protocol SceneCoordinator: Coordinator {

    var scenePrefix: String { get }

    var currentScene: Coordinator? { get }

    func changeScene(route: Route)

    func sceneRoute(route: Route) -> Route

}


public extension SceneCoordinator {

    public var route: Route {

        get {

            // ...

        }

        set {

            // ...

        }

    }


    // ...

}

public protocol TabBarControllerCoordinator: SceneCoordinator, UITabBarControllerDelegate {

    // ...

}


public extension TabBarControllerCoordinator {

    // ...

}


public protocol NavigationControllerCoordinator: Coordinator {

    // ...

}


public extension NavigationControllerCoordinator  {

    // ...

}

Challenges

Challenges

Subscribing view controllers

Challenges

public protocol Renderer: SubscriberType {

    associatedtype ViewModel

    func render(viewModel: ViewModel)

}


extension Renderer {

    public func newState(state: Any) {

        if let viewModel = state as? ViewModel {

            render(viewModel)

        } else {

            preconditionFailure("render(_:) does not accept right type")

        }

    }

}


extension SignInViewController: Renderer {}

extension CatalogViewController: Renderer {}

Challenges

App State → View Model

Challenges

extension SignInViewModel {

    init(_ state: AppState) {

        name = state.name

    }

}


store.subscribe(signInViewController, SignInViewModel.init)

Challenges

MovieViewController → Show V for Vendetta

Challenges

extension MovieViewModel {

    static func createForMovie(id: Identifier) -> (AppState) -> (MovieViewModel) {

        return { appState in

            // ...

        }

    }

}


store.subscribe(movieViewController, MovieViewModel.createForMovie(id))

Challenges

Lifecycle Hooks

Subscribe to store on viewDidLoad at earliest

Challenges

extension UIViewController {

    public class func swizzleLifecycleDelegatingViewControllerMethods() {

        dispatch_once(&Static.token) {

            cordux_swizzleMethod(#selector(viewDidLoad),

                                 swizzled: #selector(cordux_viewDidLoad))

        }

    }


    func cordux_viewDidLoad() {

        self.cordux_viewDidLoad()


        guard let vc = self as? ViewController else {

            return

        }


        vc.context?.lifecycleDelegate?.viewDidLoad?(viewController: self)

    }

}

Challenges

public protocol ViewController: class {

    var context: Context? { get }

}


public final class Context: NSObject {

    public weak var lifecycleDelegate: ViewControllerLifecycleDelegate?


    public init(_ lifecycleDelegate: ViewControllerLifecycleDelegate?) {

        self.lifecycleDelegate = lifecycleDelegate

    }

}

Challenges

public extension UIViewController {

    public var context: Context? {

        get {

            return objc_getAssociatedObject(self, &Keys.Context) as? Context

        }


        set {

            if let newValue = newValue {

                objc_setAssociatedObject(

                    self,

                    &Keys.Context,

                    newValue as Context?,

                    .OBJC_ASSOCIATION_RETAIN_NONATOMIC

                )

            }

        }

    }

Challenges

signInViewController.context = Context(lifecycleDelegate: self)


extension AuthenticationCoordinator: ViewControllerLifecycleDelegate {

    @objc func viewDidLoad(viewController viewController: UIViewController) {

        if viewController === signInViewController {

            store.subscribe(signInViewController, SignInViewModel.init)

        }

    }

}

Challenges

Route with View Controller

movies/v-for-vendetta

Challenges

public final class Context: NSObject {

    public let routeSegment: RouteConvertible

    public weak var lifecycleDelegate: ViewControllerLifecycleDelegate?


    public init(_ routeSegment: RouteConvertible,

                  lifecycleDelegate: ViewControllerLifecycleDelegate?) {

        self.routeSegment = routeSegment

        self.lifecycleDelegate = lifecycleDelegate

    }

}

Challenges

Updating Route

pop vc from nav, tap on tab

Challenges

public final class Store<State : StateType>: StoreType {

    public func setRoute<T>(action: RouteAction<T>) {

        state.route = reduce(action, route: state.route)

    }


    public func route<T>(action: RouteAction<T>) {

        state.route = reduce(action, route: state.route)

        dispatch(action)

    }


    public func dispatch(action: Action) {

        state = reducer._handleAction(action, state: state) as! State

        subscriptions.forEach {

            $0.subscriber?._newState($0.transform?(state) ?? state)

        }

    }


    // ...

}

Prototype

willowtreeapps/cordux