Cycle 0.0.19

Cycle 0.0.19

TestsTested
LangLanguage SwiftSwift
License MIT
ReleasedLast Release Sep 2018
SPMSupports SPM

Maintained by Brian Semiglia.



 
Depends on:
RxSwift~> 4.3.0
Changeset= 3.1
 

Cycle 0.0.19

Cycle.swift

Version License Platform Carthage compatible

Overview

Cycle provides a means of writing an application as a function that reduces a stream of events to a stream of effects. The stream of effects can be thought of as a reel of film that can be fed to hardware to be projected. The approach allows for consistant execution and greater observability-of/control-over state. CycleMonitor is a companion development tool that can be used to achieve that observability and control over your application.

Anatomy

Label Purpose
Frame A struct representing the state of the entire application at a given moment
Frame Filter A function that converts frames to driver-specific models that have been stripped of redundancies
Driver An isolated, stateless object that renders frames to hardware and deliver events
Event A driver-specific enum expressing events experienced by drivers
Event Filter A function that produces frames based on an input event

Composition

  1. Frames arrive as inputs to the main function.
  2. Frames are routed to frame-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 Frames are fed to a event-filter to produce a new Frame.
  6. The new Frame is input to another execution of the main function and a cycle is produced.
frame ---------> driver ----------> event + previous frames --> new frame
         
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 Frames 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

Anatomy In-Depth

Frame

The Frame 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-sizes/positions/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 event/frame-filters.

Frame-Filter

A frame-filter function allows for applying changes to a received Frame 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 frame has already been rendered, a model can be created with some sort of no-op value instead. In order to access the previous n frames 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).

Event-Filter

An event-filter function allows for the creation of a new Frame based on an incoming Event and the previous Frames. The use of the Rx withLatestFrom can be used to access the first previous frame. The use of the Rx scan operator can also be used to reach Framesfurther back in time should they be necessary. 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

Doing One Thing Well

It's said that good code does one thing well but what is 'doing' and what is a 'thing'? Verifying that something is being done can be achieved by observing a change so it might be fair to say that 'doing' is 'changing'. A change requires a before and an after so a 'thing' might be defined as a type of value that can vary and that can be compared. Putting it altogether, a revised definition might read 'Good code defines a single, verifiable transformation'. Functional programming embraces this philosphy by prioritizing types and visibility. Pure functions take one value, produce a new value and return it without making any invisible changes (side-effects). Cycle attempts to make an application a single, verifiable transformation of Events to Frames.

An exception to this within Cycle is the use of RxSwift. RxSwift isn't completely functional as it allows creating points of persistent state within streams (a side-effect that can affect subsequent executions) but it leans heavily toward dealing in terms of value and visibility.

Change without Change

A transformation is the goal, however functional programming also 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 a thin layer of 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 more 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.

Commands as Values

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 commands as values, they can be used as you might 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 type of application model.
  */
  associatedtype Frame
  
  /*
    Defines initial values of application model.
  */
  static var seed: Frame { get }

  /* 
    Defines drivers that handle frames, 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, RxUIApplicationDelegate, is included with Cycle.
  */
  associatedtype Drivers: UIApplicationDelegateProviding, ScreenDrivable

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

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

Example

  1. Subclass CycledApplicationDelegate and provide an IORouter.
@UIApplicationMain class Example: CycledApplicationDelegate<MyRouter> {
  init() {
    super.init(router: MyRouter())
  }
}

struct MyRouter: IORouter {

  static let seed = AppModel()

  struct AppModel {
    let network = Network.Model()
    let screen = Screen.Model()
    let application = RxUIApplicationDelegate.Model()
  }
  
  struct Drivers: UIApplicationDelegateProviding, ScreenDrivable {
    let network: Network
    let screen: Screen // Anything that provides a 'root' UIViewController
    let application: RxUIApplicationDelegate // Anything that conforms to UIApplicationDelegate
  }

  func driversFrom(seed: AppModel) -> Drivers { return
    Drivers(
      network = Network(model: intitial.network),
      screen = Screen(model: intitial.screen),
      application = RxUIApplicationDelegate(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
    ])
  }

}
  1. Define event-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 == (RxUIApplicationDelegate.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
    }
  }
}
  1. 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 minimal 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.

Animations

In most scenarios, an event will produce a single frame Event -> Frame. However, animated responses have a transformation signature of Event -> [Frame]. This can be handled by feeding the [Frame] into a sampled stream which then outputs an [Frame] every n seconds based on the desired frame-rate. That Frame array can then be fed to a frame-filter which strips out all but the first frame before being deliver to the drivers. The remaining frames are sent back into the stream to be rendered on the next pass. The following code provides an implementation of this:

func effectsOfEventsCapturedAfterRendering(
  incoming: Observable<[Frame]>,
  to drivers: Drivers
) -> Observable<[Frame]> {

  // Incoming models are rated-limited to 1/60th of a second
  let screenSynced = incoming.sample(
    Observable<Int>.interval(
      1.0 / 60.0,
      scheduler: MainScheduler.instance
    )
  )

  return Observable
    .merge([
      drivers
        .screen
        .eventsCapturedAfterRendering(screenSynced.map { $0.first! })
        // The state provided to the event-filter comes from the original,
        // non-rate-limited, incoming stream to ensure that the data 
        // provided isn't slightly stale the way data provided by the 
        // screenSynced stream might be.
        .withLatestFrom(incoming) { ($0.0, $0.1) }
        .animatedReducer()
      ,
      // The rest of the frames are sent back into the stream and the cycle repeats.
      screenSynced
        .withLatestFrom(incoming)
        .filter { $0.count > 1 }
        .map { $0.tail }
  ])
}

By sending the rest of the anticipated Frames back into the stream, your application has the option of re-evaluating animations based on new events during playback. Animations can thus be removed, suspended, or changed in mid-flight. The following pseudo Event -> [Frame] timelines are examples of ways that animations can be altered as they progress:

// Intercepted
.animateTo(5) -> [1, 2, 3, 4, 5]
.tick         -> [2, 3, 4, 5]
.animateTo(0) -> [2, 1, 0]
.tick         -> [1, 0]
.animateTo(4) -> [1, 2, 3, 4]
.tick         -> [2, 3, 4]
.tick         -> [3, 4]
.tick         -> [4]

// Appended
.animateTo(3) -> [0, 1, 2, 3]
.tick         -> [1, 2, 3]
.animateTo(0) -> [1, 2, 3, 2, 1, 0]
.tick         -> [2, 3, 2, 1, 0]
.animateTo(3) -> [2, 3, 2, 1, 0, 1, 2, 3]
.tick         -> [3, 2, 1, 0, 1, 2, 3]
.tick         -> [2, 1, 0, 1, 2, 3]
.tick         -> [1, 0, 1, 2, 3]
.tick         -> [0, 1, 2, 3]
.tick         -> [1, 2, 3]
.tick         -> [2, 3]
.tick         -> [3]

// Removed
.animateTo(4) -> [0, 1, 2, 3, 4]
.tick         -> [1, 2, 3, 4]
.cancel       -> [1]

The included sample application Integer Mutation Animated provides a working demonstration of this.

Related Material

Requirements

iOS 9+

Sample Apps

To run the sample apps, you'll need to build their dependencies. You can do so by opening Terminal and running carthage bootstrap.

Installation

Cycle is available via CocoaPods or Carthage. To install via Cocoapods, add the following line to your Podfile:

pod "Cycle"

License

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