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

Elm Architecture in Swift

Elm Architecture in Swift

Avatar for Yasuhiro Inami

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 $