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

Elm Architecture in Swift

Elm Architecture in Swift

Yasuhiro Inami

May 09, 2017
Tweet

More Decks by Yasuhiro Inami

Other Decks in Programming

Transcript

  1. Modern iOS Architectures • MVC, MVP, MVVM, MV* (whatever) •

    Clean Architecture • VIPER (View-Interactor-Presenter-Entity-Routing) • Why separate layers? • Less dependency (code reusability, testability) • For our understandings (sharing design philosophy)
  2. Layer Ownership • UIApplicationMain → AppDelegate • AppDelegate → Window

    → View(Controller) • View → Presenter (or ViewModel) • Presenter → UseCase (or Interactor) • UseCase → Repository → DataStore → Entity • Presenter → Wireframe (or Router)
  3. Multiple owners? protocol MyModelProtocol { ... } class MyView: UIView

    { let model: MyModelProtocol let subview: MySubView init(model: MyModelProtocol) { self.model = model // retained by ARC self.subview = MySubView(model: model) // pass model ... } } let model = MyModel(...) let view1 = MyView1(model: model) // pass model let view2 = MyView2(model: model) // pass model ... // pass, pass, pass... (manually)
  4. class MyView: UIView, NibLoadable { // using Interface Builder var

    model: MyModelProtocol? { // optional + `var` didSet { self.subview.model = model // pass model via setter } } private(set) var subview: MySubView? // optional + `var` override func awakeFromNib() { super.awakeFromNib() self.subview = MySubView.loadFromNib() ... } } let model = MyModel(...) let view1 = MyView1.loadFromNib() view1.model = model // pass model via setter ... // pass, pass, pass...
  5. Let it be singleton? class GodModel { static let shared

    = GodModel() // lives forever } class MyView: UIView { // no `let model` override init() { // no arguments super.init() self.subview = MySubview() print(GodModel.shared.message) // can call from anywhere } } let view = MyView() // no arguments to pass
  6. Model1 ~> (Web API) ~> Model2 Model2 ~> (Database) ~>

    Model3 Model3 + Model4 ~> (Calculation) ~> Model5
  7. Model1 ~> (Web API) ~> Model2 Model2 ~> (Database) ~>

    Model3 Model3 + Model4 ~> (Calculation) ~> Model5 ... Model6 ~> (Web API) ~> Model7 Model8 ~> (Database) ~> Model9 Model9 + Model10 ~> (Calculation) ~> Model11 ... Model101 ~> (Web API) ~> Model102 Model102 ~> (Database) ~> Model103 Model103 + Model104 ~> (Calculation) ~> Model105 ... Model1001 ~> (Web API) ~> Model1002 Model1002 ~> (Database) ~> Model1003 Model1003 + Model1004 ~> (Calculation) ~> ... ...
  8. React + Redux • React: Renders view in replace of

    stateful DOM • Virtual DOM: Efficient diff-patch algorithm • Redux: Singleton state container • Reducer: State transition using pure function • Middleware: Generates side-effect • Action, Reducer, and Middleware defines app's domain • Popular as a functional programming approach
  9. Elm

  10. Elm • Functional programming language for web app • Generates

    HTML/CSS/JavaScript • Purely functional, static typing, strict evaluation • No typeclass (protocol) nor FRP = easy to understand • Uses Virtual DOM & Effect Manager • Unidirectional dataflow known as Elm Architecture
  11. main = Html.beginnerProgram { model = 0, view = view,

    update = update } type Msg = Increment | Decrement update msg model = case msg of Increment -> model + 1 Decrement -> model - 1 view model = div [] [ button [ onClick Decrement ] [ text "-" ] , div [] [ text (toString model) ] , button [ onClick Increment ] [ text "+" ] ]
  12. Elm (V.S. React + Redux) • Elm's VirtualDOM doesn't... •

    own state (no setState) • manage lifecycle (no componentDidMount) • Redux is already built-in (Effect Manager) • Better effect handling inside pure "update (reducer)", not "middleware" • Typed (no propTypes validation)
  13. let program = BeginnerProgram(model: 0, view: view, update: update) ...

    enum Msg { case increment, decrement } func update(msg: Msg, model: Model) -> Model { switch msg { case .increment: return model + 1 case .decrement: return model - 1 } } func view(model: Model) -> Html<Msg> { return div(children: [ button(attributes: [onClick(.decrement)], children: [text("-")]), div(children: [text("\(model)")]), button(attributes: [onClick(.increment)], children: [text("+")]) ]) }
  14. VTree • https://github.com/inamiy/VTree • VirtualDOM for UIKit • Inspired by

    Matt-Esch/virtual-dom • Diff & Patch • func diff(old: VTree, new: VTree) -> Patch • func apply(patch: Patch, to: UIView) -> UIView?
  15. Example var model = 0 var tree = render(model) //

    virtual view var view = tree.createView() // real view timer(1) { model += 1 let newTree = render(model) let patch = diff(old: tree, new: newTree) view = apply(patch: patch, to: view) }
  16. protocol VTree { associatedtype ViewType: View associatedtype MsgType: Message var

    key: Key? { get } // for efficient reordering var props: [String: Any] { get } // can be mapped by Mirror & KVC var propsKeysForMeasure: [String] { get } // for flexbox measurement var flexbox: Flexbox.Node? { get } // for view layout var handlers: HandlerMapping<MsgType> { get } // e.g. target-action var gestures: [GestureEvent<MsgType>] { get } var children: [AnyVTree<MsgType>] { get } func createView<Msg2: Message>(_ msgMapper: @escaping (MsgType) -> Msg2) -> ViewType } class AnyVTree<Msg: Message>: VTree { ... } // type-erasure
  17. Flexbox • inamiy/Flexbox • CSS Flexbox layout engine • Swift

    wrapper of facebook/yoga • Cross-platform, used in ReactNative • Originally from joshaber/SwiftBox • Can calculate asynchronously
  18. State Machine • Manages app state (Model) and handles input

    (Msg) via state-transition function (update) that may include additional side-effect (Cmd) • That is, Mealy Machine (transducer) • A prototype of Redux since 1955 • Expressed as 6-tuple (Σ, Ω, S, s0, δ, λ)
  19. Mealy Machine • Σ = Set of Inputs • Ω

    = Set of Outputs • S = Set of States • s0 = Initial state (s0 ∈ S) • δ = State transition function, δ: S x Σ → S • λ = Output function, λ: S x Σ → Ω
  20. Elm works as Mealy Machine program : { init :

    (model, Cmd msg) , update : msg -> model -> (model, Cmd msg) , subscriptions : model -> Sub msg , view : model -> Html msg } -> Program Never model msg "update" has (almost) the same type as (Σ, S) -> (S, Ω)
  21. ReactiveAutomaton • https://github.com/inamiy/ReactiveAutomaton • Uses ReactiveSwift (FRP) for easy event

    handling • Also have RxSwift version • Related Talks • iOSDC Japan 2016 (Japanese) • iOSConf SG 2016 (English)
  22. Sample code (Login flow) • State: LoggedOut, LoggingIn, LoggedIn, LoggingOut

    • Input: Login, LoginOK, Logout, LogoutOK, ForceLogout
  23. Sample code (Login flow) // 1. switch-case pattern matching let

    mapping: EffectMapping = { fromState, input in switch (fromState, input) { case (.loggedOut, .login): return (.loggingIn, loginOKProducer) case (.loggingIn, .loginOK): return (.loggedIn, .empty) case (.loggedIn, .logout): return (.loggingOut, logoutOKProducer) case (.loggingOut, .logoutOK): return (.loggedOut, .empty) case (.loggingIn, .forceLogout), (.loggedIn, .forceLogout): return (.loggingOut, forceLogoutOKProducer) default: return nil } }
  24. Sample code (Login flow) let canForceLogout: State -> Bool =

    [.loggingIn, .loggedIn].contains // 2. Fancy pattern matching using Swift's custom operators let mappings: [EffectMapping] = [ /* input | fromState => toState | effect */ /* ---------------------------------------------------------- */ .login | .loggedOut => .loggingIn | loginOKProducer, .loginOK | .loggingIn => .loggedIn | .empty, .logout | .loggedIn => .loggingOut | logoutOKProducer, .logoutOK | .loggingOut => .loggedOut | .empty, .forceLogout | canForceLogout => .loggingOut | forceLogoutOKProducer ]
  25. Template Metaprogramming • krzysztofzablocki/Sourcery • Uses SourceKitten (AST parser) &

    Stencil (template) • Code-generate to reduces verbose code • AutoEquatable, AutoHashable, AutoCases, AutoLenses, Auto-LinuxMain.swift, etc • Used to extract user-defined enum Msg case-functions to evaluate from SwiftElm runtime
  26. Equatable Function • dankogai/peekFunc ✨"✨ func peekFunc<A, R>(_ f: (A)

    -> R) -> (fp: Int, ctx: Int) { let (_, low) = unsafeBitCast(f, to: (Int, Int).self) let offset = MemoryLayout<Int>.size == 8 ? 16 : 12 let ptr = UnsafePointer<Int>(bitPattern: low + offset) return (ptr!.pointee, ptr!.successor().pointee) } • Required for comparing enum Msg case-functions • Use FuncBox<A, R> wrapper to avoid reabstraction thunk
  27. Recap • Modern iOS Architecture ... Layer ownership problem •

    New Big Wave ! ... React + Redux, Elm • VTree (Virtual DOM) • ReactiveAutomaton (State Machine) • ... and some hacks ✨#✨ • Elm Architecture in Swift $