[译] 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前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索