Cycle.swift
概述
Cycle提供了一种方法,将应用程序编写为函数,该函数将事件流缩减为效果流。可以将效果流视为可以馈送到硬件进行投影的电影胶片。该方法允许一致的执行和更大的可观察性/控制状态。CycleMonitor 是一个辅佐开发工具,可用于实现对该应用程序的可观察性和控制。
解剖
标签 | 目的 |
---|---|
框架 | 代表某一时刻整个应用状态的struct |
框架过滤器 | 一个函数,它将框架转换成具有冗余的驱动器特定模型 |
驱动器 | 一个隔离的、无状态的对象,将框架渲染到硬件并交付事件 |
事件 | 表示驱动器经历的事件的驱动器特定enum |
事件过滤器 | 根据输入事件产生框架的函数 |
组合
帧
作为输入传递到主函数。帧
被路由到针对驱动器
生成特定模型的帧过滤函数。- 将
模型
提供给每个驱动器
以将其渲染到硬件。 驱动器
在接收到时交付事件
。事件
与之前的n帧
一起输入到事件过滤器中,以生成新的帧
。- 新的
帧
作为另一个主函数执行的输入,从而产生循环。
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
概念
目标是创建一个具有声明性和程序性之间清晰且统一边界的应用程序。声明性方面可以理解为基于传入的事件时间线的帧
时间线,这些时间线交错在一起可以按如下方式可视化
这些时间线的程序性渲染可以按如下方式可视化
深入解剖
帧
帧
简单地代表给定时刻应用程序的状态的struct。其值可以存储可能期望对象通常会维护的任何内容,例如视图大小/位置/颜色、导航遍历、项选择等。理想情况下,应避免存储可以从其他值派生的值。如果性能是一个问题,由于事件/帧过滤器的绝大部分是相对引用透明的,因此可能存在缓存/记忆化值的潜力。
帧过滤器
帧过滤器函数允许在渲染之前对接收到的帧进行更改。有两种常见的过滤器
-
将您应用程序特定的模型转换为驱动程序特定的一个。这种设计防止了任何特定的驱动程序对任何特定的全局域的依赖,基本上是应用了依赖倒置原则。
-
一个相等检查以防止不必要的渲染。如果所需的框架已经渲染,可以使用某种类型的无操作值创建一个模型。为了访问之前 n 帧的相等检查,可以使用
scan
Rx 运算符。这也意味着Driver
应该提供这样的过滤器,因为过滤器的实现将取决于Driver
的private
实现。无论哪种方式,这种过滤器将提供一种确定性的函数来管理Driver
状态。
驱动程序
驱动程序是无状态的对象,简单地接收一个值,将其渲染到硬件,并以硬件体验的方式来输出 Event
值。它们理想地有一个公共函数 eventsCapturedAfterRendering(model: RxSwift.Observable<Driver.Model>) -> RxSwift.Observable<Driver.Event>
(除了初始化函数之外)。它们还理想地没有概念超越它们的接口,避免对全局单例/类型的引用,并拥有它们自己的模型;这将是依赖倒置原则的另一个应用。
事件
事件是简单的枚举值,也可能包含硬件接收到的关联值。事件理想地由 Driver
而不是在应用级别定义和拥有(依赖倒置)。
事件过滤器
事件过滤器函数允许根据传入的事件和之前的帧创建一个新的 Frame
。可以使用 Rx withLatestFrom
来访问第一个之前的帧。如果需要,还可以使用 Rx scan
操作符来达到更早的帧。这在需要更大背景的确定络中进行判断很有用。例如,可以通过检查最后 n 次触摸坐标来识别触摸手势。
推理
做好一件事
人们常说好代码每一次只做好一件事,但什么是‘做’什么是‘事’呢?通过观察一个变化来验证某事正在被执行,因此可以公平地认为‘做’就是‘改变’。一个变化需要一个‘之前’和一个‘之后’,因此‘事’可能被定义为一种可以变化且可以被比较的价值类型。把它们放在一起,修订的定义可能如下所示:'好的代码定义了一种单一的、可验证的转换'。函数式编程拥抱这一理念,通过优先考虑类型和可见性。纯函数接受一个值,产生一个新的值并返回它,而不做任何不可见的更改(副作用)。Cycle试图通过使应用程序成为从《事件》到《帧》的单个可验证转换来实现这一点。
在Cycle中,有一个例外是使用RxSwift。RxSwift并不完全函数式,因为它允许在流中创建持久状态点(一个可能会影响后续执行的操作的副作用),但它很大程度上倾向于根据价值和可见性进行交易。
没有改变的改变
转换是目标,但函数式编程也反对可变性。如何在不改变的情况下改变呢?Cycle试图通过像翻书一样的模型来回答这个问题。正如电影中的每一帧都是不变的,视图模型也是如此。变化仅在一次帧被喂入投影仪并经过光线渲染后才产生。同样地,Cycle提供了必要的框架,以将无限列表的视图模型馈送到一个薄薄的驱动程序层,按程序进行渲染。
真理
对象通常维护自己的真相版本。这可能导致许多真相,有时是相互冲突的。这些冲突可能导致陈旧/错误的数据持续存在。一个单一真相源为所有人提供一致性。同时,将状态从对象中移除消除了它们的身份,使它们变得更具重用性和可处置性。例如,一个不可见的视图可以在不丢失承载数据的情况下被释放/重用。
视角
回到翻书哲学,更复杂的动画还包括声音的使用。光线和声音作为两种同时渲染的视角,创造出物理凝聚的错觉。这种错觉是由于这些媒介之间没有任何物理依赖。在Cycle架构中,驱动器是应用程序状态的视角。
此外,视角不必局限于单一媒介。例如,一个作为嵌套树视图实现的屏幕,可以被实现为一个由嵌套模型支持的独立视图数组。这将防止子视图界面的更改向上传播到其父视图、祖父母等,同时仍然允许协调渲染。如果扩大规模,这有可能产生一个应用程序,其中只有一种委托的程度。
以自我为中心的视角
正如纸张和赛璐璐并不专属于电影的目的,驱动器也与应用程序的意图无关。驱动器设定了自己的合同(视图-模型)和输出的事件。应用程序模型的变化不会破坏其驱动器的设计。驱动器设计的变化会破坏应用程序的设计。这产生了驱动器之间的模块化。
命令作为值
动画中的帧容易被理解为值,但它们也可以被理解为给定时刻投影机的指令。通过将指令作为值存储,它们可以像帧一样使用(验证、倒放、节流、过滤、拼接和重放);所有这些都有利于成为有用的开发工具。
实时广播
当应用到时间线的不确定性时,翻书模型会稍微有些断裂。动画的每一帧在播放前通常都是已知的,但由于驱动器提供了有限的可能事件集,这种不确定性可以受到限制,并为每个动作提供生成下一帧的手段。
接口
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>
}
示例
- 继承自CycledApplicationDelegate,并提供一个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
])
}
}
- 定义事件过滤器。
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
}
}
}
- 定义驱动器,接受一个effect-models流,并可以产生一个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))
}
}
包含了一个著名的'Counter'应用的示例项目。
动画
在大多数情况下,一个事件将产生一个单一的帧 Event -> Frame
。然而,动画响应有一个变换签名 Event -> [Frame]
。这可以通过将[Frame]
传递到一个采样流中,它随后根据所需的帧速率每隔 n 秒输出一个 [Frame]
来处理。然后,这个Frame
数组可以输入到帧过滤器中,从中删除除第一个帧之外的所有帧,然后再发送到驱动器。剩余的帧被送回流中,在下一次传递时渲染。下面的代码提供了这个实现的示例
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 }
])
}
通过将剩余的Frames
送回流中,您的应用程序可以选择在播放时根据新事件重新评估动画。动画可以在飞行中删除、暂停或更改。以下伪Event -> [Frame]
时间线是动画在进步中修改方式的示例
// 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]
包含的示例应用程序《整数变异动画》提供了一种有效的演示。
相关资料
- 边界,作者:Gary Bernhardt
- Cycle.js
- 单向数据流架构,Andre Staltz - AtTheFrontend 2016
- 单向用户界面架构 - Andre Staltz
- Redux, ReSwift
- Elm 架构
- 图灵机
需求
iOS 9+
示例应用
要运行示例应用,您需要构建其依赖项。可以通过打开终端并运行carthage bootstrap
来实现。
安装
Cycle可通过CocoaPods或Carthage获取。要通过CocoaPods安装,请将以下行添加到Podfile中
pod "Cycle"
许可协议
Cycle采用MIT许可证。更多信息请参阅LICENSE文件。