- 原文地址: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、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。