快速应用中对架构模式的需求

随着最近对Combine和SwiftUI的介绍,我们将在代码库中面临一些过渡期。 我们的应用程序将同时使用Combine和第三方响应框架,或者同时使用UIKit / AppKit和SwiftUI。 这使得随着时间的流逝难以保证一致的体系结构。 很难知道何时将这些新技术结合到我们的项目中。 从一开始就正确选择体系结构可能会大大简化将来的过渡。

传统的架构模式(例如MVC , MVP或MVVM)主要负责UI层。 当以统一的方式在您的应用程序内部混合上述技术时,它们不会有太大帮助。 例如,UIKit应用程序中的MVVM将在很大程度上依赖双向绑定技术,并具有反应性扩展,例如RxCocoa或ReactiveCocoa。 随着您逐步引入SwiftUI和Combine,事实将变得不那么正确。 很有可能在您的应用程序中拥有多个架构范例。

VIPER更加完整,因为它描述了场景之间的路由机制以及模型(实体)与管理它们的业务规则(交互器)之间的分离。 这种模式执行了“ 清洁体系结构”中有关关注点和依赖项管理分离的原则。 但是,与MVxx模式一样,当逐步采用Combine或SwiftUI时,它不能保证语法和范例在时间上的一致性。

SwiftUI将状态视为事实的唯一来源,并对状态突变做出反应。 这允许以声明的方式而不是冗长且易于出错的命令性方式来编写视图。

近年来,围绕状态概念出现了几种架构模式: Redux或MVI之类的东西以及更普遍的单向数据流架构。 有时他们会提出以中央方式(有时是局部方式)来管理国家。 这些都是很好的模式,它们非常适合将国家作为唯一真理来源的思想。 我敢肯定,他们在SwiftUI的制作中具有很大的影响力。

我本人已经在生产应用程序中实现了其中一些模式。 他们向我介绍了函数式编程,因为它们依赖于不变性,纯函数,函数组成等概念。 功能编程和状态管理非常吻合。 功能编程与数据不变性相关,因此状态也应如此。

但是,我遇到了这类架构的一些弊端,这些弊端使我发现了反馈回路系统。

什么是反馈回路系统?

反馈回路是一种系统,能够通过将其计算所得的值用作自身的下一个输入来进行自我调节,并根据给定的规则不断调整该值(反馈回路用于电子等领域,以自动调整电平例如信号)。




swift 集成 reactnative swift combine框架_rxswift


陈述这种方式听起来可能模糊不清,并且与软件工程无关, 但是 “根据某些规则调整值”正是为程序以及扩展为应用程序而设计的! 应用程序是我们要调节的各种状态的总和,以便遵循精确的规则提供一致的行为。

状态机描述描述从一个值到另一个值的允许过渡的规则。

什么是状态机?

它是一台抽象机,在任何给定时间都可以恰好处于有限数量的状态之一。 状态机可以响应某些外部输入而从一种状态变为另一种状态。 从一种状态到另一种状态的转换称为过渡。 状态机由其状态,其初始状态以及每个转换的条件的列表定义。

由于应用程序是状态机,因此它可以由反馈回路系统驱动。

反馈环基于三个部分:初始状态,反馈和减速器。 为了说明它们中的每一个,我们将依靠一个基本示例:一个从0到10计数的系统。

  1. 初始状态 :这是计数器的初始值0。
  2. 反馈 :这是我们应用于柜台以实现目标的规则。 反馈的输出是对计数器进行更改的请求。 如果0 <= counter <10,那么我们要求增加它,否则我们要求停止它。
  3. 减速器 :这是我们系统的状态机。 它描述了给定其先前值的所有可能的计数器转换以及由反馈计算出的请求。 例如:如果先前的值为0,并且请求增加该值,则新的值为1; 如果前一个是1,并且请求增加它,那么新值是2; 等等等等。 当反馈的请求停止时,则将先前的值作为新值返回。


swift 集成 reactnative swift combine框架_ui_02


反馈是唯一可以执行副作用(网络,本地I / O,UI呈现,无论您执行什么操作来访问或更改循环本地范围之外的状态)的地方。 相反,reduceer是一个纯函数,仅在给定前一个值和转换请求的情况下才可以产生新值。 禁止在还原剂中产生副作用,因为这会损害其可重复性。

传统单向数据流体系结构的缺点

如您所见,反馈回路范式的特殊之处在于状态既是输入又是输出,两者都作为一个整体连接在一起形成一个单向回路。 只要循环是活动的并且正在运行,我们就可以将状态用作本地缓存来持久化数据。

典型的用例是浏览分页的API。 这种系统允许使用当前状态来始终使上一页URL和下一页URL可以访问,这与将其存储在其他位置无关。

在更传统的单向数据流体系结构中,状态仅是系统的输出。 输入是触发副作用然后声明突变的“用户意图”。


swift 集成 reactnative swift combine框架_UI_03


我经历过几种类型的体系结构(Redux和MVI),发现自己陷入了两个主要问题:

  1. 不使用状态作为输入会导致在UI层或缓存存储库中维护本地状态。
  2. 依靠经常是枚举的诸如Intent或Action之类的输入,迫使我们用`switch`语句对它们进行解析,以确定要执行的副作用。 当我们添加新的意图或新的动作时,我们必须更改它们的解析方式,这与SOLID规则的“打开/关闭”原理背道而驰。

我并不是说这些问题是使用这些体系结构的“不可行”,也不是说我已经以最好的方式使用了它们。 例如,我研究了MVI的一种变体,其中用“命令模式”代替了意图。 每个命令负责其自己的副作用执行。 没有解析,命令是自给自足的。 这种方法符合“打开/关闭”原则,因为添加新功能就是添加新命令来执行。 而不修改意图或操作的解析方式。

但是,我赞成采用一种自然解决这些问题的方法:反馈回路系统,而不是为了满足我的需要而扭曲这些体系结构。

什么是自旋?

让我们回到我们的主要关注点:提供一种架构模型,该模型可以吸收我们在应用程序中可以期望的技术差异。

如我们所见,反馈循环是一种非常通用的模式。 这将帮助我们减轻混合技术的影响。 但是我们需要一种方法来以统一的方式声明反馈循环,而不管底层的反应框架或选择的UI技术如何。 这就是Spin发挥作用的地方。

Spin 是一个在基于Swift的应用程序中构建反馈循环的工具,无论您使用底层反应式编程框架还是使用任何Apple UI技术(RxSwift,ReactiveSwift,Combin和UIKit,AppKit,SwiftUI),都可以使用统一语法。

让我们尝试通过构建一个调节两个整数以使其收敛到平均值的系统来旋转 (例如某种系统,该系统将调整立体声扬声器上的左右音频通道以使其收敛到同一水平)。

我们将需要一个用于状态的数据类型:


struct Levels  {
    let left : Int
    let right : Int
}


我们还将需要一种数据类型来描述要在Levels上执行的转换:


enum Event  {
    case increaseLeft
    case decreaseLeft 
    case increaseRight
    case decreaseRight
}


为了描述控制过渡的状态机,我们需要一个reducer函数:


func levelsReducer (currentLevels: Levels, event: Event) -> Levels {

	guard currentLevels. left != currentLevels. right else { return currentLevels }

	switch event {
	    case .decreaseLeft:
	        return Levels ( left : currentLevels. left - 1 , right : currentLevels. right )
	    case .increaseLeft:
	        return Levels ( left : currentLevels. left + 1 , right : currentLevels. right )
	    case .decreaseRight:
	        return Levels ( left : currentLevels. left , right : currentLevels. right - 1 )
	    case .increaseRight:
	        return Levels ( left : currentLevels. left , right : currentLevels. right + 1 )
	}
}


到目前为止,代码与特定的反应式框架无关,这很棒。

让我们写两个对每个级别都有影响的反馈。

使用RxSwift


func leftEffect (inputLevels: Levels) -> Observable < Event > {
    // this is the stop condition to our Spin
    guard inputLevels. left != inputLevels. right else { return .empty() }

    // this is the regulation for the left level
    if inputLevels. left < inputLevels. right {
        return .just(.increaseLeft)
    }  else {
        return .just(.decreaseLeft)
    }
}

func rightEffect (inputLevels: Levels) -> Observable < Event > {
    // this is the stop condition to our Spin
    guard inputLevels. left != inputLevels. right else { return .empty() }

    // this is the regulation for the right level
    if inputLevels. right < inputLevels. left {
        return .just(.increaseRight)
    }  else {
        return .just(.decreaseRight)
    }
}


使用ReactiveSwift


func leftEffect (inputLevels: Levels) -> SignalProducer < Event , Never > {
    // this is the stop condition to our Spin
    guard inputLevels. left != inputLevels. right else { return .empty }

    // this is the regulation for the left level
    if inputLevels. left < inputLevels. right {
        return SignalProducer (value: .increaseLeft)
    }  else {
        return SignalProducer (value: .decreaseLeft)
    }
}

func rightEffect (inputLevels: Levels) -> SignalProducer < Event , Never > {
    // this is the stop condition to our Spin
    guard inputLevels. left != inputLevels. right else { return .empty }

    // this is the regulation for the right level
    if inputLevels. right < inputLevels. left {
        return SignalProducer (value: .increaseRight)
    }  else {
        return SignalProducer (value: .decreaseRight)
    }
}


合并


func leftEffect (inputLevels: Levels) -> AnyPublisher < Event , Never > {
    // this is the stop condition to our Spin
    guard inputLevels. left != inputLevels. right else { return Empty ().eraseToAnyPublisher() }

    // this is the regulation for the left level
    if inputLevels. left < inputLevels. right {
        return Just (.increaseLeft).eraseToAnyPublisher()
    }  else {
        return Just (.decreaseLeft).eraseToAnyPublisher()
    }
}

func rightEffect (inputLevels: Levels) -> AnyPublisher < Event , Never > {
    // this is the stop condition to our Spin
    guard inputLevels. left != inputLevels. right else { return Empty ().eraseToAnyPublisher() }

    // this is the regulation for the right level
    if inputLevels. right < inputLevels. left {
        return Just (.increaseRight).eraseToAnyPublisher()
    }  else {
        return Just (.decreaseRight).eraseToAnyPublisher()
    }
}


无论您选择哪种反应技术,编写反馈循环(也称为Spin )都非常简单:


let levelsSpin = Spinner
    .initialState( Levels ( left : 10 , right : 20 ))
    .feedback( Feedback (effect: leftEffect))
    .feedback( Feedback (effect: rightEffect))
    .reducer( Reducer (levelsReducer))


而已。 您可以在应用程序的一部分中使用RxSwift,在另一应用程序中使用Combine,所有反馈循环将使用相同的语法。

对于“类似于DSL”的语法爱好者,还有一种更具声明性的方法:


let levelsSpin = Spin (initialState: Levels ( left : 10 , right : 20 ), reducer: Reducer (levelsReducer)) {
    Feedback (effect: leftEffect)
    Feedback (effect: rightEffect)
}


如何开始循环?


// With RxSwift
Observable
    .start(spin: levelsSpin)
    .disposed(by: disposeBag)

// With ReactiveSwift
SignalProducer
    .start(spin: levelsSpin)
    .disposed(by: disposeBag)

// With Combine
AnyPublisher
    .start(spin: levelsSpin)
    .store( in : &cancellables)


混合反应框架不再是问题issue。

在UI透视图中使用Spin

尽管反馈循环本身可以不存在任何可视化而存在,但在我们的开发人员世界中,将其用作产生将在屏幕上呈现的State的方式以及处理用户发出的事件的方式更有意义。

幸运的是,将状态作为输入来渲染和返回用户交互中的事件流看起来很像反馈的定义,并且我们知道如何处理反馈😁,当然是自旋的。

一旦构建了Spin / feedback循环,我们就可以使用专门用于UI渲染/交互的新反馈来“修饰”它。 存在一种特殊的Spin来执行这种装饰:UISpin。

作为全局图片,我们可以使用此图说明UI上下文中的反馈循环:


swift 集成 reactnative swift combine框架_UI_04


假设在ViewController中,您具有如下渲染功能:


func render (state: State) {
    switch state {
    case .increasing( let value):
        self .counterLabel.text = "\(value)"
        self .counterLabel.textColor = .green
    case .decreasing( let value):
        self .counterLabel.text = "\(value)"
        self .counterLabel.textColor = .red
    }
}


我们需要用UISpin装饰“商务”旋转(例如,在viewDidLoad函数中)。


// previously defined or injected: counterSpin is the Spin that handles our counter rules
self .uiSpin = UISpin (spin: counterSpin)
// self.uiSpin is now able to handle UI side effects
// we now want to attach the UI Spin to the rendering function of the ViewController:
self .uiSpin.render(on: self , using: { $ 0 .render(state:) })
// And once the view is ready (in “viewDidLoad” function for instance) let’s start the loop:
self .uiSpin.start()
// the underlying reactive stream will be disposed once the uiSpin will be deinit


在循环中发送事件非常简单; 只需使用发出功能:


self .uiSpin.emit( Event .startCounter)


那么SwiftUI呢?

由于SwiftUI依赖于状态与视图之间的绑定的思想并负责呈现,因此连接SwiftUI Spin的方式略有不同,甚至更简单。

在您看来,您必须使用“ @ObservedObject”注释SwiftUI Spin变量:


@ ObservedObject
private var uiSpin: SwiftUISpin < State , Event > = {
    // previously defined or injected: counterSpin is the Spin that handles our counter business
    let spin = SwiftUISpin (spin: counterSpin)
    spin.start()
    return spin
}()


然后,您可以在视图内使用“ uiSpin.state”属性显示数据,并使用uiSpin.emit()发送事件。 由于SwiftUISpin也是一个“ ObservableObject”,因此每个状态更改都会触发视图渲染。


Button (action: {
    self .uiSpin.emit( Event .startCounter)
}) {
    Text ( "\(self.uiSpin.state.isCounterPaused ? " Start ": " Stop ")" )
}

// A SwiftUISpin can also be used to produce SwiftUI bindings:
Toggle (isOn: self .uiSpin.binding( for : \.isPaused, event: .toggle) {
    Text ( "toggle" )
}
       
// \.isPaused is a keypath which designates a sub state of the state,
// and .toggle is the event to emit when the Toggle is changed.


UIKit(AppKit)和SwiftUI使用UISpin的方式非常相似,允许您将先前为UIKit屏幕编写的反馈循环集成到新的SwiftUI组件中。

混合UI范型不再是问题👍。

结论

我们已经达到了目标:提出一种架构模式实现,可以简化新技术之间的过渡。

在Spinners组织中,您可以找到2个演示应用程序,它们演示了如何将Spin与RxSwift,ReactiveSwift和Combine结合使用。

  1. 基本的计数器应用程序: UIKit版本和SwiftUI版本
  2. 使用依赖项注入和协调器模式(UIKit)的更高级的“基于网络”应用程序: UIKit版本和SwiftUI版本