- 原文地址:Reactive iOS Programming: Lightweight State Containers in Swift
- 原文做者:Tyler Tillage
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:deepmissea
- 校对者:FlyOceanFish
在客户端架构如何工做上,每个 iOS 和 MacOS 开发者都有不一样的细微看法。从最经典的苹果框架所内嵌的
MVC 模式(读做:臃肿的视图控制器),到那些 MV* 模式(好比 MVP,MVVM),再到听起来有点吓人的 Viper,那么咱们该如何选择?html
这篇文章并不会回答你的问题,由于正确的答案是依据环境而定的。我想要强调的是一个我很喜欢而且常常看到的基本方法,名为状态容器。前端
实质上,状态容器只是一个围绕信息的封装,是数据安全输入输出的守护者。他们不是特别在乎数据的类型和来源。可是他们很是在乎的是当数据改变的时候。状态容器的中心思想就是,任何因为状态改变产生的影响都应该以有组织而且可预测这种方式在应用里传递。react
状态容器以与线程锁相同的方式提供安全的状态。android
这并非一个新的概念,并且它也不是一个你能够集成到整个应用的工具包。状态容器的理念是很是通用的,它能够融入进任何应用程序架构,而无需太多的附加规则。可是它是一个强大的方法,是不少流行库(好比ReactiveReSwift)的核心,好比 ReSwift、Redux、Flux 等等,这些框架的成功和绝对数量说明了状态容器模式在现代移动应用中的有效性。ios
就像 ReSwift
这样的响应式库,状态容器将 Action
和 View
之间的缺口桥联为单向数据流的一部分。然而即便没有其余两个组件,状态容器也很强力。实际上,他们能够作的比这些库使用的更多。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
的初始化器。编程
为了读取咱们状态容器的当前值,咱们要建立一个计算属性 value
。redux
因为状态改变一般是由多线程触发并读取的,因此咱们要经过 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
里任何值的改变了!
下面是一个如何使用咱们新的 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:)
把视图控制器附加到状态。实际上,这个操做能够在 viewDidLoad
,init
或者任何你想要开始监听的时候来作。
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,如今咱们正在得到好的东东。咱们能够把如今所须要的一切装在状态容器里,而且保持能够随时随地使用。
key
用于与后备存储关联。Type
,通知它应该如何保持。init(_defaultValue:key:)
方法。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:)
UserDefaults.standard
是否已经包含一个由 key
对应的值。Data
,那么使用 NSKeyedUnarchiver
解压,它会被 NSCoding
存储,而后咱们当即使用它。UserDefaults.standard
里没有和 key
匹配的值,咱们就使用已提供的 defaultValue
。didModify()
Type
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
}
}复制代码
NSCoding
,咱们就须要使用 NSKeyedArchiver
来把它转换成 Data
,而后保存它。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 应用程序。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。