Boxing (二)
目錄
⦿ 回顧
⦿ Boxing
⦿ PropertyWrapper Boxing
⦿ Modify PropertyWrapper Boxing
回顧
在前篇文章中,跟大家分享了 Boxing, Boxing 是一個 class,是用來監控當一個屬性狀態改變時,另一個屬性也隨之變化,這在程式設計中稱為 Observer Pattern(觀察者模式)。
相信大家都使用過 Notification Center 來去加入監聽,好比另篇文章中,教大家監聽當鍵盤彈出時,計算鍵盤擋住輸入框多少,以便讓 View 提高多少,不過 Notification Center 也有不便利之處,除了要加入監聽,仍得發出通知,並在發出通知時做些處理,雖然鍵盤彈出時有預設的發出通知,如下:
center.addObserver(self, selector: #selector(keyboardShown),
name: UIResponder.keyboardWillShowNotification,
object: nil)
UIResponder.keyboardWillShowNotification
是 UIKit 預設的 Notification.name,我們就不用自己 post,但很多時候狀態改變須自己發出通知,比方在使用 CoreBluetooth 時,我們會希望 peripheral(外部設備)狀態改變時發出通知,比方說設備已與 APP 連線,或設備斷線時,都需要發出通知,讓 APP 進行下一個動作,例如解析 characteristic,或是重新搜尋藍芽裝置。
除了使用 Notification Center,在 KVO 中,因為繼承了 NSObject 的 class,就可以以 Key-Value 去做到當 Value 改變時,會影響什麼其他屬性,同樣地,不需要自己發出通知。
於是,我們可以用 Boxing 來做到 Data Binding
,當屬性的狀態、數值改變了,UI 就會跟著變化,並且這個邏輯全都寫在同一個區塊中,程式碼變得簡潔易理解,好維護,好追蹤。
雖然上面提到狀態與數值,不過在真實世界中,IoT 領域的狀態
與數值
可能有些許不同,比方說清淨機的開關叫做狀態,pm2.5 的值稱為數值,我們比較需要時時刻刻監控的是開關,pm2.5 的數值不用到時時刻刻都那麼準確,這是因為頻率的問題,關於 IoT 的設計概念將來有機會再分享。
繼續閱讀|回目錄
Boxing
回到 Boing:
final class Box<T> {
typealias Listener = (T) -> Void
var listener: Listener? // Single Binding
var value: T {
didSet {
listener?(value). // Single Binding
}
}
init(_ value: T) {
self.value = value
}
func bind(_ listener: @escaping Listener) {
self.listener = listener // Single Binding
listener(value)
}
}
當時我們說用這種方式,只能做到單個監聽,比方如下:
private var isSelected = Box(false)
isSelected.bind { isSelected in
if isSelected {
self.bindingBtn.setTitle("選中", for: .normal)
} else {
self.bindingBtn.setTitle("未選", for: .normal)
}
}
isSelected.bind { selected in
if selected {
self.anotherBindingBtn.setImage(UIImage(systemName: "plus"), for: .normal)
} else {
self.anotherBindingBtn.setImage(UIImage(systemName: "minus"), for: .normal)
}
}
如果 isSelected 是我們要監控的狀態,如果在這個 Boxing 下,我們並不能分開去綁定(bind)兩個 Button,後面寫的會覆蓋掉前面寫的,或說下面的會覆蓋掉上面的。
要來解決這個問題,我們改寫如下:
final class Box<T> {
typealias Listener = (T) -> Void
var listeners: [Listener] = [] // Multi Binding
var value: T {
didSet {
// Multi Binding
listeners.forEach({ listener in
listener(value)
})
}
}
init(_ value: T) {
self.value = value
}
func bind(_ listener: @escaping Listener) {
self.listeners.append(listener) // Multi Binding
listener(value)
}
}
同樣地,我們需要在 value 改變了,listener 也能夠知道,於是將 value 帶入 listener 這個閉包,以及希望在綁定(bind)時,知道 value 的當前值或初值。
不過,這時的 listeners 是複數的,所以把這些閉包放到陣列中,當然 value 改變了,每個監控此 value 的 listener 都必須要知道,並且在綁定(bind)時,這些閉包也知道 value 的當前值或初值。
繼續閱讀|回目錄
PropertyWrapper Boxing
不過使用 Boxing,在宣告屬性並初始化時,會像這樣 private var isChanged = Box(false)
,不過這樣的宣告方式可能不是很好看,如果我們用 PropertyWrapper 包裝 Boxing 的話呢?
看到下面:
@propertyWrapper
struct Boxed<T> {
typealias Listener = (T) -> Void
private var box: Box<T>
var wrappedValue: T {
get {
return box.value
}
set {
box.value = newValue
}
}
init(wrappedValue: T) {
self.box = Box(wrappedValue)
}
func bind(_ listener: @escaping Listener) {
box.bind(listener)
}
}
當我們用屬性包裝器
去包裝 Boxing,在內部的 box 就是 Box
,而屬性包裝器的 Boxed 宣告時,實際上必須要訪問到 wrappedValue,當 wrappedValue get 時,會得到 box.value,set 的時 box.value 為 newValue。
初始化時,這個 box 的值為 Box(wrappedValue
),且新的 bind Function 為 box 去調用 bind,並把 Boxed 的 listener 帶進去。
最後,可以這樣使用它。
@Boxed private var isSelected: Bool = false
_isSelected.bind { isSelected in
if isSelected {
self.bindingBtn.setTitle("選中", for: .normal)
} else {
self.bindingBtn.setTitle("未選", for: .normal)
}
}
_isSelected 即是去訪問 wrapperValue。
繼續閱讀|回目錄
Modify PropertyWrapper Boxing
不過我們可能不希望既要寫 Boxing
,又要再寫 PropertyWrapper
,試著把它湊在一起看看。
@propertyWrapper
struct Boxed<T> {
typealias Listener = (T) -> Void
private var listeners: [Listener] = []
var wrappedValue: T {
didSet {
// Notify all listeners
listeners.forEach({ listener in
listener(wrappedValue)
})
}
}
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
var projectedValue: Boxed<T> {
return self
}
mutating func bind(_ listener: @escaping Listener) {
self.listeners.append(listener)
listener(wrappedValue)
}
}
即是說這個 wrappedValue 不再經過 Box,它裡面就有 Listener,當它狀態改變時 Listener 就會知道,然而使用 bind,也會知道當前或初始的值,由於這裡也設置了 projectedValue,所以最終在訪問時可以使用 _
或 $
來去做綁定,使用如下:
@Boxed private var isSelected: Bool = false
_isSelected.bind { isSelected in
if isSelected {
self.bindingBtn.setTitle("選中", for: .normal)
} else {
self.bindingBtn.setTitle("未選", for: .normal)
}
}
$isSelected.bind { selected in
if selected {
self.anotherBindingBtn.setImage(UIImage(systemName: "plus"), for: .normal)
} else {
self.anotherBindingBtn.setImage(UIImage(systemName: "minus"), for: .normal)
}
}
這次就分享到這,感謝您的閱讀。
繼續閱讀|回目錄