[Swift] KVC (Key-Value-Coding)
KVC란, Key-Value-Coding을 의미하며,
캡슐화된 객체에서 getter나 setter를 통한 직접 접근이 아닌
Key, KeyPath값을 통해 인스턴스의 value(property)에 간접적으로 접근할 수 있도록 NSKeyValueCoding protocol에 의해 제공되는 machanism
KVC에서 핵심인 Key와 KeyPath에 대해 알아보자
Key
우선 Key는 위에서 보았듯이, value(property)에 간접적으로 접근 할 수 있게 해준다.
key값은 property의 이름과 같아야하며,
ASCII 코드의 String이다
하지만, Key의 경우에 Objective-C의 문법으로 Swift에서 사용할 경우
Objective-C의 최상위 Root Class인 NSObject를 상속 받아야한다.
NSObject는 기본적으로 NSKeyValueCoding protocol을 채택하고 있다.
또한, 변수는 @objc 타입으로 선언하여야 한다.
class Address: NSObject {
@objc var cityName: String?
var roadName: String?
}
let address = Address()
address.value(forKey: "cityName") //nil
address.setValue("seoul", forKey: "cityName")
address.value(forKey: "cityName") //seoul
address.value(forKey: "roadName") //Error
@objc타입으로 선언하지 않은 변수에 경우에는 Key를 사용할 수 없다.
KeyPath
Key와 같이 문자열이지만, (.)으로 구분을 짓는방식이며,
상위 객체를 기준으로 하위 객체의 property로 계층적으로 접근하는 방식이다.
\Type.property_name
<keyPath Syntax>
struct Address {
var cityName: String
}
struct Person {
var name: String
var address: Address
}
let address = Address(cityName: "seoul")
let person = Person(name: "Jung", address: address)
person[keyPath: \.address.cityName] //seoul
//keyPath를 활용한 간접 접근 방식
person.address.cityName //seoul
//직접 접근 방식
위의 <KeyPath Syntax> 에서는 Type을 쓰라 하였지만,
person 자체가 Person 타입이기에 생략이 가능하다.
하지만 다른 곳에 사용하는 경우 Type을 명시 해주어야한다.
KeyPath에는 다음과 같은 종류가 있다.
- AnyKeyPath
- PartialKeyPath<Root>
- KeyPath<Root, Value> : only read
- WritableKeyPath<Root, Value>
- ReferenceWritableKeyPath<Root, Value>
WriteableKeyPath
value type 인스턴스에 사용이 가능하며, 변경 가능한 모든 property에 대해 read write가 가능하다.
var address = Address(cityName: "seoul")
var person = Person(name: "Jung", address: address)
let writableKeyPath = \Person.address.cityName
/* 2개가 같은 표현임.
let addresKeyPath = \Person.address
let cityNameKeyPath = addresKeyPath.appending(path: \.cityName)
*/
person[keyPath: writableKeyPath] = "busan"
person[keyPath: writableKeyPath] //busan
이때 만약에 property가 let이거나 let으로 instance를 생성한 경우 keyPath로 자동으로 인식된다.
KeyPath
struct Address{
var cityName: String
}
struct Person{
let name: String // let Property
var address: Address
}
var address = Address(cityName: "seoul")
var person = Person(name: "Jung", address: address)
let keyPath = \Person.name
person[keyPath: keyPath]
person[keyPath: keyPath] = "ulsan" //Error
Person의 name property가 let으로 선언이 되었기 때문에,
keyPath가 read만 가능한 KeyPath타입으로 된다.
let address2 = Address(cityName: "busan")
address2[keyPath: \.cityName] = "ulsan" //Error
여기서도 Error가 뜨는데 이는 Swift의 Struct는
Value 타입이기 때문에 keyPath가 read만 가능한 KeyPath타입으로 된다.
ReferenceWritableKeyPath
Class 인스턴스에 사용이 가능하며, 변경 가능한 모든 property에 대해 read write가 가능하다.
class Address{
var cityName: String
init(cityName: String) {
self.cityName = cityName
}
}
class Person{
let name: String // let property
var address: Address
init(name: String, address: Address) {
self.name = name
self.address = address
}
}
var address = Address(cityName: "seoul")
var person = Person(name: "Jung", address: address)
let referenceWritableKeyPath = \Person.address.cityName
person[keyPath: referenceWritableKeyPath]
person[keyPath: referenceWritableKeyPath] = "ulsan"
반면!
let keyPath = \Person.name
person[keyPath: keyPath] = "Kim" //Error
Person 의 name property가 let 이기 때문에,
keypath가 KeyPath타입으로 된다!
let address2 = Address(cityName: "busan")
let referenceWritableKeyPath = \Address.cityName
address2[keyPath: referenceWritableKeyPath] = "ulsan"
address2[keyPath: referenceWritableKeyPath] // ulsan
위의 경우에는 Error가 나지 않는데,
그 이유는 Swift의 class는 reference 타입이기 때문이다!
즉 인스턴스의 reference를 바꾸지 못하는 것일뿐 내부 property의 값은 (var 타입이라면!) 바꿀수 있다.