[譯] iOS 響應式編程:Swift 中的輕量級狀態容器

iOS 響應式編程:Swift 中的輕量級狀態容器

事物的狀態

在客戶端架構如何工做上,每個 iOS 和 MacOS 開發者都有不一樣的細微看法。從最經典的蘋果框架所內嵌的
MVC 模式(讀做:臃腫的視圖控制器),到那些 MV* 模式(好比 MVP,MVVM),再到聽起來有點嚇人的 Viper,那麼咱們該如何選擇?html

這篇文章並不會回答你的問題,由於正確的答案是依據環境而定的。我想要強調的是一個我很喜歡而且常常看到的基本方法,名爲狀態容器前端

狀態容器是什麼?

實質上,狀態容器只是一個圍繞信息的封裝,是數據安全輸入輸出的守護者。他們不是特別在乎數據的類型和來源。可是他們很是在乎的是當數據改變的時候。狀態容器的中心思想就是,任何因爲狀態改變產生的影響都應該以有組織而且可預測這種方式在應用裏傳遞。react

狀態容器以與線程鎖相同的方式提供安全的狀態。android

這並非一個新的概念,並且它也不是一個你能夠集成到整個應用的工具包。狀態容器的理念是很是通用的,它能夠融入進任何應用程序架構,而無需太多的附加規則。可是它是一個強大的方法,是不少流行庫(好比ReactiveReSwift)的核心,好比 ReSwiftReduxFlux 等等,這些框架的成功和絕對數量說明了狀態容器模式在現代移動應用中的有效性。ios

就像 ReSwift 這樣的響應式庫,狀態容器將 ActionView 之間的缺口橋聯爲單向數據流的一部分。然而即便沒有其餘兩個組件,狀態容器也很強力。實際上,他們能夠作的比這些庫使用的更多。git

在這篇文章中,我會演示一個基本的狀態容器實現,我已經把它用於各類沒有引入大型架構庫的項目中。github

構建一個狀態容器

讓咱們從構建一個基本的 State 類開始。web

/// Wraps a piece of state.
class State<Type> {

    /// Unique key used to identify the state across the application.
    let key: String
    /// Holds the state itself.
    fileprivate var _value: Type

    /// Used to synchronize changes to the state value.
    fileprivate let lockQueue: DispatchQueue

    /// Create a state container with the provided `defaultValue`, and associate it with a `key`.
    init(_ defaultValue: Type, key: String) {
        self._value = defaultValue
        self.key = key
        self.lockQueue = DispatchQueue(label: "com.stateContainers.\(key)", attributes: .concurrent)
    }

    /// Invoke this method after manipulating the state.
    func didModify() {
        print("State for key \(self.key) modified.")
    }
}複製代碼

這個基類封裝了一個任何 Type_value,經過一個 key 關聯,並聲明瞭一個提供 defaultValue 的初始化器。編程

讀取狀態

爲了讀取咱們狀態容器的當前值,咱們要建立一個計算屬性 valueredux

因爲狀態改變一般是由多線程觸發並讀取的,因此咱們要經過 GCD 使用一個讀寫鎖來確保訪問內部 _value 屬性時的線程安全。

extension State {

    /// The current state value.
    var value: Type {
        var retVal: Type!
        self.lockQueue.sync {
            retVal = self._value // I wish there was a `sync` method that inferred a generic return value.
        }
        return retVal
    }
}複製代碼

改變狀態

爲了改變狀態,咱們還要建立一個 modify(_newValue:) 函數。雖然咱們能夠容許直接訪問設置器,但在這裏的目的是圍繞狀態改變來定義結構。在使用簡單屬性設置器的方法中,經過與咱們 API 通訊修改狀態產生的影響。所以,全部的狀態改變都必須經過這個方法來達成。

extension State {

    /// Modifies the receiver by assigning the `newValue`.
    func modify(_ newValue: Type) {
        self.lockQueue.async(flags: .barrier) {
            self._value = newValue
        }

        // Handle the repercussions of the modificationself.
        didModify()
    }
}複製代碼

爲了有趣一些,咱們自定義一個運算符!

/// Modifies the receiver by assigning the right-hand side of the operator.
func ~> <T>(lhs: State<T>, rhs: T) {
    lhs.modify(rhs)
}複製代碼

關於 didModify() 方法

didModify() 是咱們狀態容器中最重要的一部分,由於它容許咱們定義在狀態改變後所觸發的行爲。爲了可以在任什麼時候候這種狀況發生時可以執行自定義的邏輯,State 的子類能夠覆蓋這個方法。

didModify() 也扮演着另外一個角色。若是咱們通用的 Type 是一個 class,狀態器就能夠無需知道它就能夠更改它的屬性。所以,咱們暴露出 didModify() 方法,以便這些類型的更改能夠手動傳播(見下文)。

這是在處理狀態時使用引用類型的固有危險,因此我建議儘量使用值類型。

使用狀態容器

下面是如何使用咱們 State 類的最基本的例子:

// State wrapping a value type
let themeColor = State(UIColor.blue, key: "themeColor")
print(themeColor.value) // "UIExtendedSRGBColorSpace 0 0 1 1"複製代碼

咱們也可使用可選類型:

// State wrapping an optional value type
let appRating = State<Int?>(nil, key: "appRating")
print(String(describing: appRating.value)) // "nil"複製代碼

改變狀態很容易:

appRating.modify(4)
print(String(describing: appRating.value)) // "Optional(4)"

appRating ~> nil
print(String(describing: appRating.value)) // "nil"複製代碼

若是咱們有無價值的類型(好比在狀態改變時,不觸發 didSet 的類型),咱們調用 didModify() 方法,讓 State 知道這個改變:

classCEO : CustomDebugStringConvertible {
    var name: String

    init(name: String) {
        self.name = name
    }

    var debugDescription: String {
        return name
    }
}

// State wrapping a reference type
let currentCEO = State(CEO(name: "John Sculley"), key: "currentCEO")
print(currentCEO.value) // "John Sculley"
// 分配一個新的用戶屬性,不須要調用 `didSet`
currentCEO ~> CEO(name: "Steve Jobs")
print(currentCEO.value) // "Steve Jobs"
// 就地修改用戶,須要手動調用 `didSet`
currentCEO.value.name = "Tim Cook"
currentCEO.didModify()
print(currentCEO.value) // "Tim Cook"複製代碼

手動調用 didModify() 是很差的,由於沒法知道引用類型的內部屬性是否改變,由於他們是能夠現場(in-place)改變的,若是你有好的方法,@我 @TTillage!

監聽狀態的改變

如今咱們已經創建了一個基本的狀態容器,讓咱們來擴展一下,讓它更強大。經過咱們的 didModify() 方法,咱們能夠用特定子類的形式添加功能。讓咱們添加一種方式,來「監聽」狀態的改變,這樣咱們的 UI 組件能夠在發生更改時自動更新。

定義一個 StateListener

第一步,讓咱們定義一個這樣的狀態監聽器:

protocol StateListener : AnyObject {

    /// Invoked when state is modified.
    func stateModified<T>(_ state: State<T>)

    /// The queue to use when dispatching state modification messages. Defaults to the main queue.
    var stateListenerQueue: DispatchQueue { get }
}

extension StateListener {

    var stateListenerQueue: DispatchQueue {
        return DispatchQueue.main
    }
}複製代碼

在狀態改變時,監聽器會在它選擇的 stateListenerQueue 上收到 stateModified(_state:) 調用,默認是 DispatchQueue.main

建立 MonitoredState 的子類

下一步,咱們定義一個專門的子類,叫作 MonitoredState,它會對監聽器保持弱引用,並通知他們狀態的改變。一個簡單的實現方式是使用 NSHashTable.weakObjects()

class MonitoredState<Type> : State<Type> {

    /// Weak references to all the state listeners.
    fileprivate let listeners: NSHashTable<AnyObject>

    /// Used to synchronize changes to the listeners.
    fileprivate let listenerLockQueue: DispatchQueue

    /// Create a state container with the provided `defaultValue`, and associate it with a `key`.
    override init(_ defaultValue: Type, key: String) {
        self.listeners = NSHashTable<AnyObject>.weakObjects()
        self.listenerLockQueue = DispatchQueue(label: "com.stateContainers.listeners.\(key)", attributes: .concurrent)
        super.init(defaultValue, key: key)
    }

    /// All of the listeners associated with the receiver.
    var allListeners: [StateListener] {
        var retVal: [StateListener] = []
        self.listenerLockQueue.sync {
            retVal = self.listeners.allObjects.map({ $0 as? StateListener }).flatMap({ $0 }) // remove `nil` values
        }
        return retVal
    }

    /// Notifies all listeners that something changed.
    override func didModify() {
        super.didModify()

        let allListeners = self.allListeners

        let state = self
        for l in allListeners {
            l.stateListenerQueue.async {
                l.stateModified(state)
            }
        }
    }
}複製代碼

不管什麼時候 didModify 被調用,咱們的 MonitoredState 類調用 stateModified(_state:) 上的監聽者,簡單!

爲了添加監聽器,咱們要定義一個 attach(listener:) 方法。和上面的內容很像,在咱們的 listeners 屬性上,使用 listenerLockQueue 來設置一個讀寫鎖。

extension MonitoredState {

    /// Associate a listener with the receiver's changes. func attach(listener: StateListener) { self.listenerLockQueue.sync(flags: .barrier) { self.listeners.add(listener as AnyObject) } } }複製代碼

如今能夠監放任何封裝在 MonitoredState 裏任何值的改變了!

根據狀態的改變來觸發 UI 的更新

下面是一個如何使用咱們新的 MonitoredState 類的例子。假設咱們在 MonitoredState 容器中追蹤設備的位置:

/// The device's current location. let deviceLocation = MonitoredState<CLLocation?>(nil, key: "deviceLocation")複製代碼

咱們還須要一個視圖控制器來展現當前設備在地圖上的位置:

// Centers a map on the devices's current locationclass LocationViewController : UIViewController { @IBOutlet var mapView: MKMapView! override func viewDidLoad() { super.viewDidLoad() self.updateMapForCurrentLocation() } func updateMapForCurrentLocation() { if let currentLocation = deviceLocation.value { // Center the map on the device's location
            self.mapView.setCenter(currentLocation.coordinate, animated: true)
        }
    }
}複製代碼

因爲咱們須要在 deviceLocation 改變的時候更新地圖,因此要把 LocationViewController 擴展爲一個 StateListener

extension LocationViewController : StateListener {

    func stateModified<T>(_state: State<T>) {
        ifstate === deviceLocation {
            print("Location changed, updating UI")
            self.updateMapForCurrentLocation()
        }
    }
}複製代碼

而後記住使用 attach(listener:) 把視圖控制器附加到狀態。實際上,這個操做能夠在 viewDidLoadinit 或者任何你想要開始監聽的時候來作。

let vc = LocationViewController()
deviceLocation.attach(listener: vc)複製代碼

如今咱們正監聽 deviceLocation,一旦咱們從 CoreLocation 獲得一個新的定位,咱們所要作的只是改變咱們的狀態容器,咱們的視圖控制器會自動的更新位置!

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    if let closestLocation = locations.first {
        // Triggers `updateMapForCurrentLocation` on the VC asynchronously on the main queue
        deviceLocation ~> closestLocation
    }
}複製代碼

值得注意的是,因爲咱們使用了一個弱引用 NSHashTable,在視圖控制器被銷燬時,allListeners 屬性永遠也不會有 deviceLocation。沒有必要「移除」監聽器。

記住,在真實的使用場景裏,要確保視圖控制器的 view 在執行更新 UI 以前是可見的。

保持狀態

OK,如今咱們正在得到好的東東。咱們能夠把如今所須要的一切裝在狀態容器裏,而且保持能夠隨時隨地使用。

  1. 咱們如今有一個惟一的 key 用於與後備存儲關聯。
  2. 咱們知道值的 Type,通知它應該如何保持。
  3. 咱們知道何時值須要從存儲器中加載,使用 init(_defaultValue:key:) 方法。
  4. 咱們知道何時值須要被保存在存儲器中,使用 didModify() 方法。

使用 UserDefaults

讓咱們建立一個狀態容器,它能夠自動地保存任何改變到 UserDefaults.standard 中,而且在初始化的時候從新加載以前的這些值。它同時支持可選類型和非可選類型。他也會自動序列化和反序列化符合 NSCoding 的類型,即便 UserDefaults 並無直接支持 NSCoding 的使用。

這裏是代碼,我會在下面講解。

class UserDefaultsState<Type> : MonitoredState<Type> {

    ///1) Loads existing value from `UserDefaults.standard`if it exists, otherwise falls back to the `defaultValue`.
    public override init(_defaultValue:Type, key:String) {
        let existingValue = UserDefaults.standard.object(forKey: key)
        if let existing = existingValue as? Type {
            //2) Non-NSCoding value
            print("Loaded \(key) from UserDefaults")
            super.init(existing, key: key)
        } elseif let data = existingValue as? Data, let decoded = NSKeyedUnarchiver.unarchiveObject(with: data) as? Type {
            //3) NSCoding value
            print("Loaded \(key) from UserDefaults")
            super.init(decoded, key: key)
        } else {
            //4) No existing value
            super.init(defaultValue, key: key)
        }
    }

    ///5) Persists any changes to `UserDefaults.standard`.
    public override func didModify() {
        super.didModify()

        let val = self.value
        if let val = val as? OptionalType, val.isNil {
            //6) Nil value
            UserDefaults.standard.removeObject(forKey:self.key)
            print("Removed \(self.key) from UserDefaults")
        } elseif let val = val as? NSCoding {
            //7) NSCoding value
            UserDefaults.standard.set(NSKeyedArchiver.archivedData(withRootObject: val), forKey:self.key)
            print("Saved \(self.key) to UserDefaults")
        } else {
            //8) Non-NSCoding value
            UserDefaults.standard.set(val, forKey:self.key)
            print("Saved \(self.key) to UserDefaults")
        }

        UserDefaults.standard.synchronize()
    }
}複製代碼

init(_defaultValue:key:)

  1. 咱們的初始化方法檢查 UserDefaults.standard 是否已經包含一個由 key 對應的值。
  2. 若是咱們能加載一個對象,而且它恰好是基本類型,咱們能夠當即使用它。
  3. 若是咱們加載的是 Data,那麼使用 NSKeyedUnarchiver 解壓,它會被 NSCoding 存儲,而後咱們當即使用它。
  4. 若是 UserDefaults.standard 裏沒有和 key 匹配的值,咱們就使用已提供的 defaultValue

didModify()

  1. 在狀態改變的時候,咱們想要自動保存咱們的狀態,這樣作的方法依賴於 Type
  2. 若是基本類型是 Optional 的,而且爲 nil,咱們只須要簡單的把值從 UserDefaults.standard 移除,檢查一個基本類型是否爲 nil 有點棘手,不過 用協議擴展 Optional 是一個解決方法:
protocol OptionalType {

    /// Whether the receiver is `nil`.var isNil: Bool { get }
}

extension Optional : OptionalType {

    publicvar isNil: Bool {
        return self == nil
    }
}複製代碼
  1. 若是咱們的值符合 NSCoding,咱們就須要使用 NSKeyedArchiver 來把它轉換成 Data,而後保存它。
  2. 除此以外,咱們只需把值直接存儲到 UserDefaults 中。

如今,若是咱們想要得到 UserDefaults 的支持,咱們要作的僅僅是使用新的 UserDefaultsState 類!

UserDefaults.standard.set(true, forKey: "isTouchIDEnabled")
UserDefaults.standard.synchronize()

let isTouchIDEnabled = UserDefaultsState(false, key: "isTouchIDEnabled")
print(isTouchIDEnabled.value) // "true"

isTouchIDEnabled ~> falseprint(UserDefaults.standard.bool(forKey: "isTouchIDEnabled")) // "false"複製代碼

咱們的 UserDefaultsState 會在其值更改時自動更新它的後臺存儲。在應用啓動的時候,它會自動把 UserDefaultsState 中的現有值投入使用。

支持其餘的數據存儲

這只是使用狀態容器的例子之一,State 如何擴展到智能地存儲本身的數據。在個人項目中,也創建了一些子類,當發生更改時,它們將異步地保留到磁盤或鑰匙串。你甚至能夠經過使用不一樣的子類來觸發與遠程服務器的同步或者將指定標記錄到分析庫中。它毫無限制。

應用級別的狀態管理

因此這些狀態容器放在哪裏呢?一般我把他們靜態儲存到一個 struct 裏,這樣能夠在整個應用裏訪問。這與基於 Flux 庫存儲全局應用狀態有些類似。

struct AppState {
    static let themeColor = State(UIColor.blue, key: "themeColor")
    static let appRating = State<Int?>(nil, key: "appRating")
    static let currentCEO = State(CEO(name: "Tim Cook"), key: "currentCEO")
    static let deviceLocation = MonitoredState<CLLocation?>(nil, key: "deviceLocation")
    static let isTouchIDEnabled = UserDefaultsState(false, key: "isTouchIDEnabled")
}複製代碼

你可使用分離或嵌入式的結構體以及不一樣的訪問級別來調整狀態容器的做用域。

結論

在狀態容器上管理狀態有不少好處。之前放在單例上的數據,或在網絡代理中傳播的數據,如今已經在高層次上浮現出來而且可見。應用程序行爲中的全部輸入都忽然變得清晰可見而且組織嚴謹。

從 API 響應到特徵切換到受保護的鑰匙串項,使用狀態容器模式是圍繞關鍵信息定義結構的優秀方式。狀態容器能夠輕鬆地用於緩存,用戶偏好,分析以及應用程序啓動之間須要保持的任何事情。

狀態容器模式讓 UI 組件不用擔憂如何以及什麼時候生成數據,並開始把焦點轉向如何把數據轉換成夢幻般的用戶體驗。

關於做者

CapTecher Tyler Tillage 位於亞特蘭大辦公室,在應用設計和開發有超過六年的經驗。 他專一於移動和 web 的前端產品,而且熱衷於使用成熟的設計模式和技術來構建卓越的用戶體驗。Tyler 曾爲每月數百萬用戶使用的零售和銀行業構建 iOS 應用程序。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索