14.%d Xcode Study — KVC / KVO Passing Data

春麗 S.T.E.M.
13 min readAug 10, 2021

--

目錄

⦿ 傳值回顧
⦿ KVC
⦿ KVC again
⦿ KVO 與 正向傳值
⦿ 建議使用的 KVO 反向傳值

傳值回顧

前篇文章中提到 UserDefaults、NotificationCenter 的傳值,傳值,我們知道分為正向反向傳值,在反向傳值中,最重要的是理解 VC、property、UIElement 的生命週期,在 VC 產生後,如果你的 VC 是從 Storyboard 產生,需在 required init? 裡加入 Observer 才起作用,但此時 Observer 觸發的 objc function 不能使用 UIElement,因為控件尚未產生。

還記得嗎?我們在 VC 中,setupUI(或 configureUI)通常會放到 viewDidLoad 去使用,這即是說 viewDidLoad 時,UIElement 已經產生,習慣上,將 IBOutlet 放在 viewDidLoad 上方,而 Properties 放在 IBOutlet 的上方,這其實是告訴你 VC、property、UIElement,物件產生的先後順序,也就是 Lifecycle(生命週期)的一部分。

NotificationNotificationCenter 的差異是,我們用 Center 做為監聽中心,監聽物件狀態的改變,當狀態改變便會觸發 objc function,在 objc function 中,我們用 Notification 去取到發出通知的物件,如:

@objc func received(sender: Notification) {
let textField = sender.object as? UITextField
let text = textField.text
let property = text

}

這個物件就是你的監聽物件,當這個物件狀態改變後便會 post(發送通知),如果你的 VC 有註冊 Observer(Listener),則觸發 objc function。

在 UserDefaults 中,我們藉著 UserDefaults().setValue(aSentence, forKey: “aEntered”) 為 aEntered 這個 key 設定一個值 aSentence,再由 bLabel.text = "\(UserDefault().string(forKey:"aEntered") ?? "none")" ,aEntered 這個 key 取出值。

把它看作系統內建的 dictionary,所以正反向傳遞皆可。

繼續閱讀|回目錄

KVC

Key-Value Coding,後面再解釋 KVC 的意思,我們在使用 KVC 傳值的 Storyboard 中的基本配置如下:

先創建一個可以使用鍵值對的物件,如下:

class KVCObject: NSObject {
@objc static var string: String = ""

}

如此除了可以像 UserDefaults 般,用 Key 去存取物件的值,且因為設定為靜態屬性,所以也可用 KVCObject.string 直接去存取物件,那麼使用 KVC,正反向傳值都無礙。

第一個頁面的程式碼如下:

    override func viewWillAppear(_ animated: Bool) {
// KVC 反向
if KVCObject.string != "" {
text = KVCObject.string
firstLabel.text = text
}

}

@IBAction func firstBtnTapped(_ sender: UIButton) {
// KVC 正向
guard let text = firstTextField.text else { return }
KVCObject.setValue(text, forKey: "string")
}

viewWillAppear 去檢查要傳過來的值,如果不為空,就將屬性設定為 KVC 物件的值,再去設定 IBOutlet。

而按下第一頁的 Button,使用 string 這個 key 去設定 KVCObject,接著跳第二頁(Storyboard Segue)。

接著看到第二頁的程式碼:

    override func viewDidLoad() {
super.viewDidLoad()
// KVC 正向
text = KVCObject.value(forKey: "string") as! String
secondLabel.text = text

}

@IBAction func secondBtnTapped(_ sender: UIButton) {
// KVC 反向
guard let text = secondTextField.text else { return }
KVCObject.string = text
self.navigationController?.popViewController(animated: true)

}

在 viewDidLoad 的地方,利用 KVC 取出來的值設定第二頁的 property text,接著再將 secondLabel.text 設定為 text

按下 Button,將輸入框的值設定給 KVCObject,接著回前頁。

最後結果如下:

但是!其實前面這種宣告方式並非 KVC 本義,至於能夠使用 Key-Value 的方式來設定值則是因為 NSKeyValueCoding Mechanism,下面,我們來看看真正的 KVC 宣告方式。

繼續閱讀|回目錄

KVC again

在 NSObject 中,物件都有個特性,透過 Key-Value 的方式去存取,但 Key 必須要與 property 相同名稱,我們直接先來宣告一個正確的 KVC 物件,如下:

class KVCObject: NSObject {
// 真 KVC 宣告
@objc dynamic var string: String!

override init() {
self.string = ""
super.init()
}
}

接著,var kvcObject = KVCObject(),接著就可以把它當作一般的 property 來使用,只不過它可以更方便地用 KeyPath 或 Key 去 setValue。

那麼,我們為了接值,兩個頁面都需要宣告 KVC 物件。

所以在第一頁的程式碼:

    // 真 KVC 物件,接值用
internal var kvcFirst = KVCObject()

override func viewWillAppear(_ animated: Bool) {
if let text = kvcFirst.value(forKey: "string") as? String,
text != "" {
firstLabel.text = text
}
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? SecondViewController,
let text = firstTextField.text {
vc.kvcSecond.setValue(text, forKey: "string")
}
}

@IBAction func unwindToFirest(segue: UIStoryboardSegue) {
if let vc = segue.source as? SecondViewController,
let text = vc.secondTextField.text {
kvcFirst.setValue(text, forKey: "string")
}
}

prepare 如同一般使用 Segue 的正向傳值,而 unwindSegue 則是反向傳值,我們通常會在 viewWillAppear 設定第一個頁面。

接著看到第二頁的程式碼:

    // 真 KVC 物件,接值用
internal var kvcSecond = KVCObject()

override func viewDidLoad() {
super.viewDidLoad()
text = kvcSecond.value(forKey: "string") as! String
secondLabel.text = text
}

第二頁只有取值與設定 Label,相當的簡便。

繼續閱讀|回目錄

KVO 與 正向傳值

那什麼是 KVO 呢?Key-Value Observing,鍵值觀察,是基於 KVC 物件設定 Observation,若這個被觀察的值變化了,我們就讓 UI 隨之改變

然而,若真要使用 KVO 傳值,這邊建議大家只使用反向傳值,還記得前一篇文章的 Notification 傳值嗎?NotificationCenter 在反向傳值時設定監聽是相當容易的,然而設定正向監聽則需在第二頁的 required init? 中,這是因為第二頁初步產生時就應該設定監聽,否則當第一頁的值改變時發出通知是沒有作用的。

即便如此,我們還是來看一下 KVO 的正向傳值,在第一頁中:

    internal var kvcFront = KVCObject()

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? SecondViewController {
// 為了正向傳值
kvcFront = vc.kvcFront
}

}

@IBAction func firstBtnTapped(_ sender: UIButton) {
performSegue(withIdentifier: "goToSecond", sender: nil)
guard let text = firstTextField.text else { return }
kvcFront.setValue(text, forKey: "string")
}

在按下 Button 的時候產生 Segue,接著再改變 kvc 的值,讓這個改變發出通知,然而這個發出通知我們無法掌控,這邊有一個 trick,就是透過 prepare 將這個欲改變的物件變成 destination 的相同 class 的物件。

是不是有點複雜?我們先再看到第二頁:

    internal var kvcFront = KVCObject()
var observation: NSKeyValueObservation?

required init?(coder: NSCoder) {
super.init(coder: coder)

observation = kvcFront.observe(\.string, options: [.new]) {
kvcObject, change in
guard let newValue = change.newValue else { return }
print("Front Passing Data Changed : ", newValue!)
self.text = newValue!
}
}

override func viewDidLoad() {
super.viewDidLoad()
secondLabel.text = text
}

deinit {
observation?.invalidate()
}

在 required init? 中設定監聽,監聽物件就是本身的 kvcFront,然而,在第一頁 performSegue 後就進到 prepare,我們在 prepare 中已將欲改變的物件令為這個監聽物件

所以在第二頁中,當物件的值改變後,我們就去改變 text,接著在 viewDidLoad 中就可以用 text 去改變 Label。

最後,當第二頁消失後,我們希望移除這個監聽。

繼續閱讀|回目錄

建議使用的 KVO 反向傳值

直接看到程式碼,在第一頁中:

    internal var kvcBack = KVCObject()
var observation: NSKeyValueObservation?

override func viewDidLoad() {
super.viewDidLoad()

observation = kvcBack.observe(\.string, options: [.new]) {
kvcFirst, change in
guard let newValue = change.newValue else { return }
print("Back Passing Data Changed : ", newValue!)
self.firstLabel.text = newValue
}
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

if let vc = segue.destination as? SecondViewController {
// 為了反向傳值
vc.kvcBack = kvcBack
}
}

deinit {
observation?.invalidate()
}

在 viewDidLoad 中監聽自己內部的物件,當這個值改變時,跟著改變 Label,而透過 Segue 轉場,所以仍然在 prepare 中,透過 destination 去將這個監聽物件賦值給 destination,這就相當於監聽第二頁的物件。

若第一頁消失,移除這個監聽。

接著看到第二頁的程式碼:

    internal var kvcBack = KVCObject()
@IBAction func secondBtnTapped(_ sender: UIButton) {
guard let text = secondTextField.text else { return }
kvcBack.setValue(text, forKey: "string")
self.navigationController?.popViewController(animated: true)
}

相當簡單, 按下 Button 後,改變這個物件的值,自然第一頁就會監聽到了,接著去改變它自己的 IBOutlet。

完成了!給自己一點掌聲吧!

這次就分享到這,感謝您的閱讀。

繼續閱讀|回目錄

附上 GitHub:

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet