Latest 0.0.9
Homepage https://github.com/BrianSemiglia/Cycle.swift
License MIT
Platforms ios 9.0
Dependencies RxSwift, Changeset
Authors

Cycle.swift

Version
License
Platform

Overview

Cycle provides a means of writing an application as a function that reduces a stream of events to a stream of effects.

Anatomy

Effect – A struct representing the state of the entire application at a given moment
Pre-Filter – A function that converts effects to driver-models that are stripped of redundancies
Driver – An isolated, stateless object that renders effects to hardware and deliver events
Event – An enum expressing events experienced by hardware
Post-Filter – A function that produces Effects based on input Events

Composition

  1. Effects arrive as inputs to the main function.
  2. Effects are routed to pre-filter functions that produce models specific to Drivers.
  3. Models are fed to each Driver to be rendered to hardware.
  4. Drivers deliver Events as they arrive.
  5. The Event along with the previous n Effects are fed to a post-filter to produce a new Effect.
  6. The new Effect is input to another execution of the main function and a cycle is produced.
effect --------> driver ----------> event + previous effects -> new effect

Network.Model -> Network                    Network.Model       Network.Model
Screen.Model  -> Screen  -> Network.Event + Screen.Model   ---> Screen.Model
Session.Model -> Session                    Session.Model       Session.Model

Concept

The goal is to produce an application that has clear and uniform boundaries between the declarative and procedural. The declarative side can be understood as a timeline of Effects based on the incoming timeline of Events which when intertwined can be visualized as such:

alt tag

The procedural rendering of those timelines can be visualized like so:

alt tag

View as higher-res SVG

In-Depth

Anatomy

Effect

The Effect is simply a struct representing the state of application at a given moment. Its value can store anything that you might expect objects to normally maintain such as view-frames/colors, navigation-traversal, item-selections, etc. Ideally, the storage of values that can be derived from other values should be avoided. If performance is a concern there is the potential for the caching/memoization of values due to the mostly-referentially-transparent nature of pre/post-filters.

Pre-Filter

A pre-filter function allows for applying changes to a received Effect before being rendered. There are two common filters:

  • A conversion from your application-specific model to a driver-specific one. This design prevents a dependency of any particular driver to any particular global domain and is basically an application of the Dependency Inversion Principle.

  • An equality check to prevent unnecessary renderings. If a desired effect has been rendered, a model can be created with some sort of no-op value instead. In order to access the previous n effects for this equality check, the scan Rx operator can be used. It would also make sense that Drivers be the providers of this sort of filter as the implementation of the filter would depend of the private implementation of the Driver. Either way, this sort of filter would provide a deterministic function for Driver state management.

Driver

Drivers are stateless objects that simply receive a value, render it to hardware and output Event values as they are experienced by hardware. They ideally have one public function eventsCapturedAfterRendering(model: RxSwift.Observable<Driver.Model>) -> RxSwift.Observable<Driver.Event> (aside from an init function). They also ideally have no concept of what is beyond their interface, avoiding references to global singletons/types and having a model that they have autonomy over; this would be another application of the Dependency Inversion Principle.

Event

Events are simple enum values that may also contain associated values received by hardware. Events are ideally defined and owned by a Driver as opposed to being defined at the application level (Dependency Inversion).

Post-Filter

A post-filter function allows for the creation of a new Effect based on an incoming Event and the current Effect. The Effect created here becomes available to the incoming Effect stream of the main function and is also how a previous Effect is accessed using the Rx scan operator. The scan operator is not limited to just the immediately preceding Effect in the timeline; any previous Effect can be accessed. This is useful for determinations that require a larger context. For example, a touch-gesture could be recognized by examining the last n number of touch-coordinates.

Reasoning

Change without Change

Applications are functions that transform values based on events. However, functional programming discourages mutability. How can something change without changing? Cycle attempts to answer that question with a flip-book like model. Just as every frame of a movie is unchanging, so can be view-models. Change is only produced once a frame is fed into a projector and run past light, or rendered rather. In the same way, Cycle provides the scaffolding necessary to feed an infinite list of view-models into drivers to be procedurally rendered.

Truth

Objects typically maintain their own version of the truth. This has the potential to lead to many truths, sometimes conflicting. These conflicts can cause stale/incorrect data to persist. A single source of truth provides consistency for all. At the same time, moving state out of objects removes their identity and makes them much reusable/disposable. For example, a view that is not visible can be freed/reused without losing the data that it was hosting.

Perspective

Going back to the flip-book philosophy, more complex animations also include the use of sound. Light and sound are two perspectives rendered in unison to create the illusion of physical cohesion. The illusion is due to the mediums having no physical dependence on one another. In the Cycle architecture, drivers are the perspectives of the application’s state.

Further, perspectives don’t have to be specific to a single medium. For example, a screen implemented as a nested-tree of views could be instead be implemented as an array of independent views backed by a nested-model. This would prevent changes to a child-view’s interface from rippling up to its parents, grandparents, etc. while still allowing for a coordinated rendering. Scaled up, this has the potential to produce an application where there is only ever one degree of delegation.

Self-Centered Perspective

Just as paper and celluloid aren’t exclusive to the purpose of movies, drivers are independent of an application’s intentions. Drivers set the terms of their contract (view-model) and the events they output. Changes to an application’s model don’t break its drivers’ design. Changes to its drivers’ design do break the application’s design. This produces modularity amongst drivers.

Values as Commands

Frames in an animation are easy to understand as values, but they can also be understood as commands for the projector at a given moment. By storing driver-commands as values, commands can be used just as frames (verified, reversed, throttled, filtered, spliced, and replayed); all of which make for useful development tools.

alt tag

Live Broadcast

The flip-book model breaks a bit when it comes to the uncertain future of an application’s timeline. Each frame of an animation is usually known before playback but because drivers provide a finite set of possible events, that uncertainty can be constrained and given the means to produce the next frame for every action.

Interface

public protocol IORouter {
  /* 
    Defines schema and initial values of application model.
  */
  associatedtype Model: Initializable

  /* 
    Defines drivers that handle effects, produce events. Requires two default drivers: 

      1. let application: UIApplicationDelegateProviding - can serve as UIApplicationDelegate
      2. let screen: ScreenDrivable - can provide a root UIViewController

    A default UIApplicationDelegateProviding driver, RxUIApplication, is included with Cycle.
  */
  associatedtype Drivers: UIApplicationDelegateProviding, ScreenDrivable

  /*
    Instantiates drivers with initial model. Necessary to for drivers that require initial values.
  */
  func driversFrom(initial: Model) -> Drivers

  /*
    Returns a stream of Model created by rendering the incoming stream of effects to drivers and then capturing and transforming their events into the Model type. See example for intended implementation.
  */
  func effectsOfEventsCapturedAfterRendering(
    incoming: Observable<Model>,
    to drivers: Drivers
  ) -> Observable<Model>
}

Example

  1. Subclass CycledApplicationDelegate and provide an IORouter.

    @UIApplicationMain class Example: CycledApplicationDelegate<MyRouter> {
    init() {
      super.init(router: MyRouter())
    }
    }
    
    struct MyRouter: IORouter {
    
    struct AppModel: Initializable {
      let network = Network.Model()
      let screen = Screen.Model()
      let application = RxUIApplication.Model()
    }
    
    struct Drivers: UIApplicationDelegateProviding, ScreenDrivable {
      let network: Network
      let screen: Screen // Anything that provides a 'root' UIViewController
      let application: RxUIApplication // Anything that conforms to UIApplicationDelegate
    }
    
    func driversFrom(initial: AppModel) -> Drivers { return
      Drivers(
        network = Network(model: intitial.network),
        screen = Screen(model: intitial.screen),
        application = RxUIApplication(model: initial.application)
      )
    }
    
    func effectsOfEventsCapturedAfterRendering(
      incoming: Observable<AppModel>,
      to drivers: Drivers
    ) -> Observable<AppModel> {
    
      let network = drivers
        .network
        .eventsCapturedAfterRendering(incoming.map { $0.network })
        .withLatestFrom(incoming) { ($0.0, $0.1) }
        .reducingFuctionOfYourChoice()
    
      let screen = drivers
        .screen
        .eventsCapturedAfterRendering(incoming.map { $0.screen })
        .withLatestFrom(incoming) { ($0.0, $0.1) }
        .reduced()
    
      let application = drivers
        .application
        .eventsCapturedAfterRendering(incoming.map { $0.application })
        .withLatestFrom(incoming) { ($0.0, $0.1) }
        .reduced()
    
      return Observable.merge([
        network,
        screen,
        application
      ])
    }
    
    }
  2. Define post-filters.

    extension ObservableType where E == (Network.Model, AppModel) {
    func reducingFuctionOfYourChoice() -> Observable<AppModel> { return
      map { event, context in
        var new = context
        switch event.state {
          case .idle:
            new.screen.button.color = .blue
          case .awaitingStart, .awaitingResponse:
            new.screen.button.color = .grey
          default: 
            break
        }
        return new
      }
    }
    }
    
    extension ObservableType where E == (Screen.Model, AppModel) {
    func reduced() -> Observable<AppModel> { return
      map { event, context in
        var new = context
        switch event.button.state {
          case .highlighted:
            new.network.state = .awaitingStart
          default: 
            break
        }
        return new
      }
    }
    }
    
    extension ObservableType where E == (RxUIApplication.Model, AppModel) {
    func reduced() -> Observable<AppModel> { return
      map { event, context in
        var new = context
        switch event.session.state {
          case .launching:
            new.screen = Screen.Model.downloadView
          default: 
            break
        }
        return new
      }
    }
    }
  3. Define drivers that, given a stream of effect-models, can produce a stream of event-models.

    class MyDriver {
    
    struct Model {
      var state: State
      enum State {
        case idle
        case sending
      }
    }
    
    enum Event {
      case receiving
    }
    
    fileprivate let output: BehaviorSubject<Model>
    
    // Pull-based interfaces (e.g. UITableViews) require retaining state.
    // State retention should be made as minimum as possible and well-guarded.
    fileprivate let model: Model
    
    public init(initial: Model) {
      model = initial
      output = BehaviorSubject<Model>(value: initial)
    }
    
    public func eventsCapturedAfterRendering(_ input: Observable<Model>) -> Observable<Event> { 
      input
        .subscribe(next: self.render)
        .disposed(by: cleanup)
      return self.output
    }
    
    func render(model: Model) {    
      if case .sending = model.state {
        // Perform side-effects...
      }
    }
    
    func didReceiveEvent() {
      output.on(.next(.receiving))
    }
    
    }

A sample project of the infamous ‘Counter’ app is included.

Related Material

Requirements

iOS 9+

Installation

Cycle is available through CocoaPods. To install
it, simply add the following line to your Podfile:

pod "Cycle"

License

Cycle is available under the MIT license. See the LICENSE file for more info.

Latest podspec

{
    "name": "Cycle",
    "version": "0.0.9",
    "summary": "An experiment in unidirectional-data-flow inspired by Cycle.js.",
    "description": "Cycle provides a means of writing an app as a filter over a stream of external events.",
    "homepage": "https://github.com/BrianSemiglia/Cycle.swift",
    "license": {
        "type": "MIT",
        "file": "LICENSE"
    },
    "authors": {
        "[email protected]": "[email protected]"
    },
    "source": {
        "git": "https://github.com/BrianSemiglia/Cycle.swift.git",
        "tag": "0.0.9"
    },
    "platforms": {
        "ios": "9.0"
    },
    "source_files": "Cycle/Classes/**/*",
    "dependencies": {
        "RxSwift": [
            "~> 3.5.0"
        ],
        "Changeset": [
            "2.1"
        ]
    },
    "pushed_with_swift_version": "3.0"
}

Pin It on Pinterest

Share This