天天看點

介紹Spin:Swift中的通用回報循環系統

快速應用中對架構模式的需求

随着最近對Combine和SwiftUI的介紹,我們将在代碼庫中面臨一些過渡期。 我們的應用程式将同時使用Combine和第三方響應架構,或者同時使用UIKit / AppKit和SwiftUI。 這使得随着時間的流逝難以保證一緻的體系結構。 很難知道何時将這些新技術結合到我們的項目中。 從一開始就正确選擇體系結構可能會大大簡化将來的過渡。

傳統的架構模式(例如MVC , MVP或MVVM)主要負責UI層。 當以統一的方式在您的應用程式内部混合上述技術時,它們不會有太大幫助。 例如,UIKit應用程式中的MVVM将在很大程度上依賴雙向綁定技術,并具有反應性擴充,例如RxCocoa或ReactiveCocoa。 随着您逐漸引入SwiftUI和Combine,事實将變得不那麼正确。 很有可能在您的應用程式中擁有多個架構範例。

VIPER更加完整,因為它描述了場景之間的路由機制以及模型(實體)與管理它們的業務規則(互動器)之間的分離。 這種模式執行了“ 清潔體系結構”中有關關注點和依賴項管理分離的原則。 但是,與MVxx模式一樣,當逐漸采用Combine或SwiftUI時,它不能保證文法和範例在時間上的一緻性。

SwiftUI将狀态視為事實的唯一來源,并對狀态突變做出反應。 這允許以聲明的方式而不是冗長且易于出錯的指令性方式來編寫視圖。

近年來,圍繞狀态概念出現了幾種架構模式: Redux或MVI之類的東西以及更普遍的單向資料流架構。 有時他們會提出以中央方式(有時是局部方式)來管理國家。 這些都是很好的模式,它們非常适合将國家作為唯一真理來源的思想。 我敢肯定,他們在SwiftUI的制作中具有很大的影響力。

我本人已經在生産應用程式中實作了其中一些模式。 他們向我介紹了函數式程式設計,因為它們依賴于不變性,純函數,函數組成等概念。 功能程式設計和狀态管理非常吻合。 功能程式設計與資料不變性相關,是以狀态也應如此。

但是,我遇到了這類架構的一些弊端,這些弊端使我發現了回報回路系統。

什麼是回報回路系統?

回報回路是一種系統,能夠通過将其計算所得的值用作自身的下一個輸入來進行自我調節,并根據給定的規則不斷調整該值(回報回路用于電子等領域,以自動調整電平例如信号)。

介紹Spin:Swift中的通用回報循環系統

陳述這種方式聽起來可能模糊不清,并且與軟體工程無關, 但是 “根據某些規則調整值”正是為程式以及擴充為應用程式而設計的! 應用程式是我們要調節的各種狀态的總和,以便遵循精确的規則提供一緻的行為。

狀态機描述描述從一個值到另一個值的允許過渡的規則。

什麼是狀态機?

它是一台抽象機,在任何給定時間都可以恰好處于有限數量的狀态之一。 狀态機可以響應某些外部輸入而從一種狀态變為另一種狀态。 從一種狀态到另一種狀态的轉換稱為過渡。 狀态機由其狀态,其初始狀态以及每個轉換的條件的清單定義。

由于應用程式是狀态機,是以它可以由回報回路系統驅動。

回報環基于三個部分:初始狀态,回報和減速器。 為了說明它們中的每一個,我們将依靠一個基本示例:一個從0到10計數的系統。

  1. 初始狀态 :這是計數器的初始值0。
  2. 回報 :這是我們應用于櫃台以實作目标的規則。 回報的輸出是對計數器進行更改的請求。 如果0 <= counter <10,那麼我們要求增加它,否則我們要求停止它。
  3. 減速器 :這是我們系統的狀态機。 它描述了給定其先前值的所有可能的計數器轉換以及由回報計算出的請求。 例如:如果先前的值為0,并且請求增加該值,則新的值為1; 如果前一個是1,并且請求增加它,那麼新值是2; 等等等等。 當回報的請求停止時,則将先前的值作為新值傳回。
介紹Spin:Swift中的通用回報循環系統

回報是唯一可以執行副作用(網絡,本地I / O,UI呈現,無論您執行什麼操作來通路或更改循環本地範圍之外的狀态)的地方。 相反,reduceer是一個純函數,僅在給定前一個值和轉換請求的情況下才可以産生新值。 禁止在還原劑中産生副作用,因為這會損害其可重複性。

傳統單向資料流體系結構的缺點

如您所見,回報回路範式的特殊之處在于狀态既是輸入又是輸出,兩者都作為一個整體連接配接在一起形成一個單向回路。 隻要循環是活動的并且正在運作,我們就可以将狀态用作本地緩存來持久化資料。

典型的用例是浏覽分頁的API。 這種系統允許使用目前狀态來始終使上一頁URL和下一頁URL可以通路,這與将其存儲在其他位置無關。

在更傳統的單向資料流體系結構中,狀态僅是系統的輸出。 輸入是觸發副作用然後聲明突變的“使用者意圖”。

介紹Spin:Swift中的通用回報循環系統

我經曆過幾種類型的體系結構(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上下文中的回報循環:

介紹Spin:Swift中的通用回報循環系統

假設在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版本

有Spin.Swift回購 。 公關當然是受歡迎的(例如⭐️😏)。

我打算開發與RxJava和Flow相容的Kotlin實作(不勝感激)。

希望您喜歡這篇文章。 随時發表評論,以便我們進行交流。

From: https://hackernoon.com/introducing-spin-a-universal-feedback-loop-system-in-swift-rg2i3yab