์ง€๋‚œ ๊ธ€์—์„œ๋Š” Objective-C์—์„œ KVC/KVO๊ฐ€ ์–ด๋–ป๊ฒŒ ์“ฐ์˜€๋Š”์ง€ ์•Œ์•„๋ณด์•˜๋‹ค. ์ด๋ฒˆ์—๋Š” Swift๋‹ค.

Key Value Coding

์—ฌ๊ธฐ๊นŒ์ง€๋Š” Objective C์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋“ค์ด์—ˆ๋‹ค. ์ด์ œ๋Š” ์‹ค์ œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” Swift์—์„œ ์–ด๋–ค์ง€ ๋ณด์ž. ์‚ฌ์‹ค ์ž์ฃผ ๋งˆ์ฃผํ•  ์ผ์€ ์—†๋‹ค. ์™œ๋ƒํ•˜๋ฉด..

struct CreditCard {
    var address: String
}
 
struct Person {
    var creditCard: CreditCard
}
 
let creditCard = CreditCard(address: "์šฉ์ธ์‹œ")
let wansik = Person(creditCard: creditCard)
 
let wansikAddress = wansik.creditCard.address

ํŠน์ • ์ฃผ์†Œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด, ์ง์ ‘์ ์œผ๋กœ ์ ‘๊ทผํ•ด์„œ ๊ฐ’์„ ์ฝ์–ด์˜ค๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์–ด๋–ป๊ฒŒ Swift์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ?

let wansikAddress = wansik[keyPath: \.address.town]
wansik[keyPath: \.address.town] = "์ˆ˜์›์‹œ"

KeyPath๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค! ๊ทธ๋ฆฌ๊ณ  ๊ฐ’์— ์ ‘๊ทผํ•ด์„œ ์ˆ˜์ •๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. Objective C์—์„œ๋Š” setValue: ๋กœ ๊ฐ€๋Šฅํ–ˆ์—ˆ๋Š”๋ฐ, ์ด์ œ๋Š” ์ง์ ‘ ๋„ฃ๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

๊ทธ๋Ÿฐ๋ฐ Objective C์—์„œ ์‚ฌ์šฉํ•˜๋˜ KeyPath์™€ ๋ญ”๊ฐ€ ๋ชจ์–‘์ด ๋‹ค๋ฅด๋‹ค. ๊ฑฐ๊ธฐ์„œ๋Š” ์ •๋ง path์˜ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง„ key์˜ ์—ญํ• (๋ฌธ์ž์—ด๊ณผ .์˜ ์ฃผํ•ฉ)์„ ํ•˜๋Š” ๋Š๋‚Œ์ด ๊ฐ•ํ–ˆ๋‹ค๋ฉด, ์—ฌ๊ธฐ์„œ๋Š” ๋ญ”๊ฐ€ ๊ทœ์น™์ด ์žˆ์–ด๋ณด์ธ๋‹ค. ๋‹ค์Œ ํฌ์ŠคํŒ…์œผ๋กœ ๋„˜๊ธฐ์ž.

Key Value Observing

ํ•ด๋‹น ๊ฐœ๋…์€ Objective C์™€ ๋™์ผํ•˜๋‹ค. ์ฆ‰ ๊ฐ์ฒด์˜ Property ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋‹ค๋ฅธ ๊ฐ์ฒด์— ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•œ ํŒจํ„ด์ด๋‹ค. Model๊ณผ View์‚ฌ์ด์— ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋ถ„๋ฆฌ๋œ ๋…€์„๋“ค์˜ Sync๋ฅผ ๋งž์ถ”๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•œ๋‹ค. Objective C ๋Ÿฐํƒ€์ž„์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— NSObject๋ฅผ ์ƒ์†ํ•œ class์—์„œ๋งŒ KVO๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

Observed Object Setup

struct CreditCard {
    var address: String
}
 
struct Person {
    var creditCard: CreditCard
}

์ž, ์•„๊นŒ๋ณด์•˜๋˜ ์˜ˆ์ œ์ด๋‹ค. ์™„์‹์ด๋ผ๋Š” ์‚ฌ๋žŒ์ด ์‹ ๋ถˆ์ž๋ผ ๋„๋ง์„ ๋‹ค๋…€์„œ ์ฃผ์†Œ๋ฅผ ๊ณ„์†ํ•ด์„œ ๋ฐ”๊พผ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณด์ž.(์–ต์ง€๋‹ค) ํšŒ์‚ฌ ์ž…์žฅ์—์„œ๋Š” ์ด๋…€์„์˜ address๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š” ์‹œ์ ์— ์•Œ๋ฆผ์„ ๋ฐ›๊ธธ ์›ํ•  ๊ฒƒ์ด๋‹ค. ๊ทธ๋ž˜์„œ ์ € ๋…€์„์„ KVO๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Observingํ•  ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ•ด์ค˜์•ผ ํ•˜๋Š” ์ž‘์—…๋“ค์ด ์žˆ๋‹ค.

class CreditCard: NSObject {
    @objc dynamic var address: String
 
    init(address: String) {
        self.address = address
    }
}
  1. class๋กœ ๋ณ€๊ฒฝ
  2. NSObject ์ƒ์†: NSObject ์ƒ์† ํด๋ž˜์Šค์—์„œ๋งŒ KVO ์‚ฌ์šฉ๊ฐ€๋Šฅ
  3. @objc ์ถ”๊ฐ€: Objective runtime ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ!
  4. dynamic modifier ์ถ”๊ฐ€: Objective C์˜ dynamic dispatch๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ!

dynamic์— ๋Œ€ํ•œ ๊ฒƒ์€ ์ถ”ํ›„ ํฌ์ŠคํŒ…์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•˜์ž. ์ด๊ฒƒ๋„ ํ•  ์–˜๊ธฐ๊ฐ€ ๋งŽ์€ ์ฃผ์ œ์ธ ๋“ฏํ•˜๋‹ค.

์ผ๋‹จ ์—ฌ๊ธฐ๊นŒ์ง€ ํ•˜๋ฉด ์ € class์˜ property๋ฅผ observingํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ๊ฐ€ ๋˜์—ˆ๋‹ค.

Observer definition

class CreditCard: NSObject {
    @objc dynamic var address: String
 
    init(address: String) {
        self.address = address
    }
}
 
class ViewController: UIViewController {
 
    private let town = ["์šฉ์ธ์‹œ", "์ˆ˜์›์‹œ", "๊ด‘์ฃผ์‹œ", "์„œ์šธ์‹œ", "ํ•˜๋‚จ์‹œ"]
    private let creditCard = CreditCard(address: "์šฉ์ธ์‹œ")
    private var observers: [NSKeyValueObservation] = []
 
    override func viewDidLoad() {
        let observer = self.creditCard.observe(\.address, options: [.old, .new]) { (object, change) in
            print(object)
            print(change.oldValue, change.newValue)
        }
        self.observers.append(observer)
    }
 
    @IBAction func buttonTouched(_ sender: Any) {
        self.creditCard.address = "\(self.town[Int.random(in: (0..<5))])์‹œ"
    }
 
}

๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ๋งˆ๋‹ค ๋„๋ง๊ฐ€๋„๋ก ํ•ด๋ณด์ž. observer ๊ฐ์ฒด๋Š” ๋ฐ›์•„์„œ, VC๊ฐ€ deinit๋˜๋Š” ์‹œ์ ์— ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋‚ ์•„๊ฐ€๋„๋ก ํ•˜์˜€๋‹ค.

<test.CreditCard: 0x60000101e480>
Optional("์šฉ์ธ์‹œ") Optional("๊ด‘์ฃผ์‹œ์‹œ")
<test.CreditCard: 0x60000101e480>
Optional("๊ด‘์ฃผ์‹œ์‹œ") Optional("์„œ์šธ์‹œ์‹œ")
<test.CreditCard: 0x60000101e480>
Optional("์„œ์šธ์‹œ์‹œ") Optional("ํ•˜๋‚จ์‹œ์‹œ")
<test.CreditCard: 0x60000101e480>
Optional("ํ•˜๋‚จ์‹œ์‹œ") Optional("ํ•˜๋‚จ์‹œ์‹œ")
<test.CreditCard: 0x60000101e480>
Optional("ํ•˜๋‚จ์‹œ์‹œ") Optional("ํ•˜๋‚จ์‹œ์‹œ")
<test.CreditCard: 0x60000101e480>
Optional("ํ•˜๋‚จ์‹œ์‹œ") Optional("๊ด‘์ฃผ์‹œ์‹œ")
<test.CreditCard: 0x60000101e480>
Optional("๊ด‘์ฃผ์‹œ์‹œ") Optional("์„œ์šธ์‹œ์‹œ")

object๋Š” ์‹ค์ œ ๊ด€์ฐฐํ•˜๊ณ  ์žˆ๋Š” ๋…€์„์„ ๋ณด๋‚ด์ฃผ๊ณ , change์—์„œ๋Š” ์ด์ „ ๊ฐ’, ์ƒˆ ๊ฐ’์„ ๋ณด๋‚ด์ฃผ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

Option

์œ„์˜ ์˜ˆ์ œ์—์„œ ๊ด€์ฐฐํ•˜๋Š” ๋Œ€์ƒ์„ keyPath๋กœ ์ค€ ๊ฒƒ์™ธ์— option์ด๋ผ๋Š” ๊ฐ’์„ ์ฃผ์—ˆ์—ˆ๋‹ค. ์ด๊ฒŒ ๋ฌด์—‡์ธ์ง€ ์•Œ์•„๋ณด์ž. NSKeyValueObservingOptions์ธ ๊ตฌ์กฐ์ฒด๋กœ ์ •์˜๋˜์–ด ์žˆ๋‹ค. .old, .new๋Š” ๋ณด์•˜์œผ๋‹ˆ ๋„˜์–ด๊ฐ€๊ณ , .initial, .prior๋งŒ ์•Œ์•„๋ณด์ž.

.initial

combine์„ ์“ฐ๋‹ค๋ณด๋ฉด, ์ดˆ๊ธฐ ์—ฐ๊ฒฐ๋œ ์‹œ๊ธฐ์— ๊ฐ’์„ ๋ฐ›์•„๋ณด๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค. ์ด ๊ฒฝ์šฐ๊ฐ€ ๋”ฑ ๊ทธ๊ฑฐ๋‹ค. ์ฒ˜์Œ์— ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์€ ์‹œ์ ์—๋„ handler๊ฐ€ ๋ถˆ๋ฆฌ๊ฒŒ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ํ•ด๋‹น ์˜ต์…˜์„ ์ผœ๋ฉด ๋œ๋‹ค.

override func viewDidLoad() {
    let observer = self.creditCard.observe(\.address, options: [.old, .new, .initial]) { (object, change) in
        print(change.oldValue, change.newValue)
    }
    self.observers.append(observer)
}
nil Optional("์šฉ์ธ์‹œ")
Optional("์šฉ์ธ์‹œ") Optional("์ˆ˜์›์‹œ")
Optional("์ˆ˜์›์‹œ") Optional("ํ•˜๋‚จ์‹œ")

์ด๋Ÿฐ์‹์œผ๋กœ newValue์— ๊ฐ’์ด ๋‹ด๊ฒจ์„œ ์ฒ˜์Œ์— ์˜ค๊ฒŒ ๋œ๋‹ค.

.prior

๋ฐ”๋กœ ์ด์ „์˜ ์ƒํƒœ๋ฅผ ๊ฐ™์ด ์ค€๋‹ค! ์ด๊ฒŒ ๋ฌด์Šจ ๋ง์ด๋ƒ๋ฉด ๋ง๊ทธ๋Œ€๋กœ ์ด์ „์— ์ถœ๋ ฅ๋œ ์นœ๊ตฌ๊นŒ์ง€ ๊ฐ™์ด ์คŒ์„ ๋งํ•œ๋‹ค.

override func viewDidLoad() {
    let observer = self.creditCard.observe(\.address, options: [.old, .new, .prior]) { (object, change) in
        if change.isPrior {
            print("์ด์ „ ์ƒํƒœ์˜ ๊ฐ’์ด์—์š”!", change.oldValue, change.newValue)
        } else {
            print("์ด๋ฒˆ์— ๋ณ€๊ฒฝ๋œ ์ƒํƒœ๊ฐ’์ด์—์š”!", change.oldValue, change.newValue)
        }
    }
    self.observers.append(observer)
}
์ด์ „ ์ƒํƒœ์˜ ๊ฐ’์ด์—์š”! Optional("์šฉ์ธ์‹œ") nil
์ด๋ฒˆ์— ๋ณ€๊ฒฝ๋œ ์ƒํƒœ๊ฐ’์ด์—์š”! Optional("์šฉ์ธ์‹œ") Optional("ํ•˜๋‚จ์‹œ")
์ด์ „ ์ƒํƒœ์˜ ๊ฐ’์ด์—์š”! Optional("ํ•˜๋‚จ์‹œ") nil
์ด๋ฒˆ์— ๋ณ€๊ฒฝ๋œ ์ƒํƒœ๊ฐ’์ด์—์š”! Optional("ํ•˜๋‚จ์‹œ") Optional("์šฉ์ธ์‹œ")
์ด์ „ ์ƒํƒœ์˜ ๊ฐ’์ด์—์š”! Optional("์šฉ์ธ์‹œ") nil
์ด๋ฒˆ์— ๋ณ€๊ฒฝ๋œ ์ƒํƒœ๊ฐ’์ด์—์š”! Optional("์šฉ์ธ์‹œ") Optional("ํ•˜๋‚จ์‹œ")
์ด์ „ ์ƒํƒœ์˜ ๊ฐ’์ด์—์š”! Optional("ํ•˜๋‚จ์‹œ") nil
์ด๋ฒˆ์— ๋ณ€๊ฒฝ๋œ ์ƒํƒœ๊ฐ’์ด์—์š”! Optional("ํ•˜๋‚จ์‹œ") Optional("์ˆ˜์›์‹œ")

์ถœ๋ ฅ์„ ๋ณด๋ฉด ๊ธˆ๋ฐฉ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค.

์žฅ๋‹จ์ 

์ผ๋‹จ ์ด ๊ธ€์„ ์ฝ๋‹ค๋ณด๋ฉด, ๋งŽ์ด๋“ค ์‚ฌ์šฉํ•˜๋Š” willSet, didSet๊ณผ ๊ฐ™์€ propert observer์™€ ๋น„์Šทํ•˜๋‹ค๋Š” ๊ฒƒ์„ ๋ˆˆ์น˜์ฑŒ ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ์ ์ด ์กด์žฌํ•˜๋Š”๋ฐ, KVO๊ฐ™์€ ๊ฒฝ์šฐ Object ์™ธ๋ถ€์—์„œ observer๋ฅผ ๊ฑธ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด๋‹ค.

์žฅ์ 

๋จผ์ €, Model, View์™€ ๊ฐ™์ด ๋‘ ๊ฐ์ฒด ์ด์ƒ์˜ ๋™๊ธฐํ™”๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๊ฑด ์•ž์—์„œ๋„ ์ด์•ผ๊ธฐ ํ•œ ๋‚ด์šฉ์ด๋‹ค.

๊ฐ์ฒด์˜ ๊ตฌํ˜„์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ , ์ƒํƒœ ๋ณ€ํ™”์— ๋Œ€์‘์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ์ฆ‰, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ๋Š”๋ฐ, ์—ฌ๊ธฐ์— ๋‚ด๋ถ€์ ์œผ๋กœ Property observer๋ฅผ ๋‹ฌ ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์ž„์—๋„ ์™ธ๋ถ€์—์„œ KVO๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค. ํ•˜์ง€๋งŒ ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์žˆ๋Š” property๊ฐ€ Objective runtime ์ œ๊ณต(@objc, dynamic)ํ•˜์ง€ ์•Š์œผ๋ฉด ์•ˆ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ์ง€..

Observed Property์˜ old value, new value๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

KeyPath๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— nested Property ๊ด€์ฐฐ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

class CreditCard: NSObject {
 
    class Address: NSObject {
        @objc dynamic var location: String
 
        init(location: String) {
            self.location = location
        }
    }
 
    @objc dynamic var address: Address
 
    init(address: Address) {
        self.address = address
    }
}
 
class ViewController: UIViewController {
 
    private let town = ["์šฉ์ธ์‹œ", "์ˆ˜์›์‹œ", "๊ด‘์ฃผ์‹œ", "์„œ์šธ์‹œ", "ํ•˜๋‚จ์‹œ"]
    private let creditCard = CreditCard(address: CreditCard.Address(location: "์šฉ์ธ์‹œ"))
    private var observers: [NSKeyValueObservation] = []
 
    override func viewDidLoad() {
        // Note!
        let observer = self.creditCard.observe(\.address.location, options: [.old, .new]) { (object, change) in
            print(change.oldValue, change.newValue)
        }
        self.observers.append(observer)
    }
 
    @IBAction func buttonTouched(_ sender: Any) {
        self.creditCard.address.location = "\(self.town[Int.random(in: (0..<5))])"
    }
 
}
Optional("์šฉ์ธ์‹œ") Optional("์šฉ์ธ์‹œ")
Optional("์šฉ์ธ์‹œ") Optional("์„œ์šธ์‹œ")
Optional("์„œ์šธ์‹œ") Optional("๊ด‘์ฃผ์‹œ")

์ด์™€ ๊ฐ™์ด keyPath๋กœ ์ ‘๊ทผํ•˜์—ฌ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๋œป์ด๋‹ค.

๋งˆ์ง€๋ง‰์œผ๋กœ ์ถ”๊ฐ€์ ์œผ๋กœ Observer๋ฅผ ํ•ด์ œํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค๋Š” ์ ์ด๋‹ค. ์ด๊ฑด ๋ชฐ๋ž๋‹ค. ์ž๋ฃŒ๋ฅผ ์ฐพ์•„๋ณด๋‹ˆ, Foundation Release Notes for macOS 10.13 and iOS 11์— โ€œRelaxed Key-Value Observing Unregistration Requirementsโ€์— ๋ช…์‹œ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค. ์ข€ ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ์ดํ•ดํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด When is KVO unregistration automatic?๊ธ€์„ ์ฐธ๊ณ ํ•˜์ž.

๋‹จ์ 

์ผ๋‹จ Objective C ๋Ÿฐํƒ€์ž„์— ์˜์กดํ•˜๊ฒŒ ๋œ๋‹ค. NSObject๋„ ์ƒ์†ํ•ด์•ผ ํ•˜๊ณ  @objc, dynamic modifier๋„ ๋‹ฌ์•„์ฃผ์–ด์•ผ ํ•œ๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ ์•Œ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด ์žˆ๋‹ค๋ฉด ์—ฌ๊ธฐ์— ์ ์–ด๋‘๋„๋ก ํ•˜๊ฒ ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

์ด๋ ‡๊ฒŒ Swift์—์„œ KVC, KVO๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊นŒ์ง€ ์ตํ˜€๋ณด์•˜๋‹ค. ๋‹ค์Œ์—๋Š” ์ด์ „์— ๋‹ค๋ฃจ์ง€ ๋ชปํ•œ ๊ฒƒ๋“ค์— ๋Œ€ํ•ด ์ ์–ด๋ณด๊ฒ ๋‹ค. ๋!

Reference