KVC 는 Key-Value Coding 의 약자이며, 객체의 값을 직접 가져오거나 수정하지 않고, Key 또는 Key Path 를 이용해서 간접적으로 데이터를 가져오거나 수정하는 방법을 말합니다.

KVC 는 Core Data 나 바인딩, 스크립팅 등을 지원하기 위한 필수 기술 중 하나이기 때문에 반드시 알고 있어야 하는 중요한 개념입니다.

@objcMembers class Kid: NSObject {
  dynamic var nickname: String = ""
  dynamic var age: Double = 0.0
  dynamic var bestFriend: Kid? = nil
  dynamic var friends: [Kid] = []
}

let ben = Kid(nickname: "Benji", age: 5.5)

let kidsNameKeyPath = #keyPath(Kid.nickname)

let name = ben.valueForKeyPath(kidsNameKeyPath)
ben.setValue("Ben", forKeyPath: kidsNameKeyPath)

Swift 3 에서 KVC 를 사용하는 방법입니다.

#keyPath() 를 통해서 KVC 를 수행할 String 을 만들어줍니다.

KVC를 사용하기 위해선 해당 인스턴스가 NSObject의 subclass여야 하고 사용하려는 property가 @objc, dynamic 속성을 가져야 합니다.


#keyPath() 는 컴파일 시에 입력된 값이 실제 존재하는 property인지 검사하지만, 결과값이 String 이기 때문에 실제 위와 같이 KVC 를 수행할 때 String의 안정성을 보장할 수 없습니다.

또한 valueForKeyPath의 리턴값과 setValue(_, forKeyPath:) 의 인자는 Any 이기 때문에 Swift 에서는 Type Casting 이 필요합니다.


Swift 의 목표 중에 하나는 모든 모호함을 제거하는 것입니다.

그리고 위의 방법은 Darwin 시스템에서만 가능합니다.


Swift 4 에서는 모호함을 제거하고, 모든 시스템을 위해 KVC 를 쉽게 사용할 수 있는 방법이 도입되었습니다.

Key Paths

먼저 #keyPath() 대신 KeyPath 를 정의하는 방법을 알아보겠습니다.

백슬래시를 시작으로 Type을 쓰고 뒤로 연결된 property 를 씁니다.

Key Path 의 여러가지 예입니다. Key Path 가 사용되는 위치에 따라 Type 은 숨김이 가능합니다.

\Kid.nickname
\.nickname
\Kid.nickname.characters

// 아래 예제는 스펙이 확정되었지만, 아직 구현되어 있지는 않습니다.(iOS11 beta4 기준)
\Kid.bestFriend?.nickname
\Kid.friends[0]
\Data.[.startIndex]
\.[.startIndex]

KeyPath 를 이용해 값을 가져오거나 수정하는 방법도 간단합니다.

let age = ben[keyPath: \Kid.age]
ben[keyPath: \Kid.nickname] = "Ben"

이제 KVC 는 올바른 타입을 리턴하기 때문에 Type Casting을 할 필요가 없습니다.


위 코드에서는 ben의 Type이 Kid이기 때문에 Key Path 에서 Kid 를 아래 코드와 같이 숨길 수 있습니다.

let age = ben[keyPath: \.age]
ben[keyPath: \.nickname] = "Ben"

맨 처음에 소개드렸던 코드를 Swift 4 방식대로 수정하면 다음과 같습니다.

class Kid {
  var nickname: String = ""
  var age: Double = 0.0
  var bestFriend: Kid? = nil
  var friends: [Kid] = []
}

let ben = Kid(nickname: "Benji", age: 5.5)

let name = ben[keyPath: \Kid.nickname]
ben[keyPath: \Kid.nickname] = "Ben"

class 가 NSObject 의 subclass 일 필요도 없고 @objc 와 dynamic 키워드도 필요없습니다.

keyPath 를 인자로 전달하는 subscript 와 비슷한 문법으로 편리하게 이용할 수 있습니다.


이제 KVC 는 class 뿐만 아니라 struct 에서도 사용할 수 있습니다.

다음 예제를 보세요. (바로 위의 코드와 이어집니다.)

struct BirthdayParty {
  let celebrant: Kid
  var theme: String
  var attending: [Kid]
}

let bensParty = BirthdayParty(celebrant: ben, theme: "Construction", attending: [])

let birthdayKid = bensParty[keyPath: \BirthdayParty.celebrant]
bensParty[KeyPath: \BirthdayParty.theme] = "Pirate"

KeyPath<Root, Value>

앞서 새로운 KVC 에서는 원하는 property의 Type 을 그대로 가져올 수 있다고 했습니다.

어떻게 이게 가능한 걸까요?


백슬래시를 이용한 새로운 Key Path 를 만들게 되면 이 값은 KeyPath<Root, Value> 라는 클래스의 인스턴스가 됩니다. Root는 Key Path의 Base Class의 Type을 의미하고 Value는 데이터를 다루게 될 Property의 Type을 의미하게 됩니다.

생성된 Key Path 는 KeyPath<Kid, String> 입니다.

Key Path 를 생성했을 때 적용할 수 있는 Type 과 리턴 또는 수정할 수 있는 Type 이 결정됩니다.

Key Path 를 변수에 저장해놓고 여러군데에서 쓸 수 있습니다.

KeyPath 더하기

다음과 같이 appending 함수로 KeyPath 를 확장할 수 있습니다.

\BirthdayParty.celebrant.appending(\Kid.age)

\BirthdayParty.celebrant => KeyPath<BirthdayParty, Kid>
\Kid.age                 => KeyPath<Kid, Double>


\BirthdayParty.celebrant.age => KeyPath<BirthdayParty, Double>

appending 이 가능한 규칙은 간단합니다. 앞의 Value Type과 더해지는 Root Type 이 같아야 합니다.

아래 그림을 확인해 보세요.

PartialKeyPath

Root Type 이 같은 여러개의 KeyPath를 하나의 타입으로 표현할 수 있습니다.

KeyPath<BirthdayParty, String>PartialKeyPath<Birthday> 라는 상위 타입으로 표현이 가능합니다.

그래서 아래와 같이 Root Type은 같지만, Value Type이 다른 여러개의 KeyPath 들을 Array로 묶을 수 있습니다.

Mutating KeyPath

생일파티의 테마값을 가져오고 수정하는 코드를 살펴보겠습니다.

let birthdayTheme = bensParty[keyPath: \BirthdayParty.theme]
bensParty[keyPath: \BirthdayParty.theme] = "Pirate"

위 코드는 전혀 문제가 없습니다.

사용하고 있는 KeyPath가 같으니 변수로 만들어서 코드를 재작성해보겠습니다.

let themeKeyPath = \BirthdayParty.theme

let birthdayTheme = bensParty[keyPath: themeKeyPath]
bensParty[keyPath: themeKeyPath] = "Pirate"

이 코드도 문제가 없습니다.

themeKeyPath 의 타입은 KeyPath 일까요?

Xcode 를 이용해 타입을 살펴보면 KeyPath 가 아닌 WritableKeyPath 임을 알 수 있습니다.

WritableKeyPath는 KeyPath의 subclass 로 데이터 수정을 가능하게 해 줍니다.

KeyPath 는 read-only 로 작동하기 때문에 데이터의 수정은 불가능합니다.


위 예제에서는 BirthdayParty.theme 가 var 로 정의되어 있기 때문에 자동으로 WritableKeyPath로 타입이 만들어졌습니다.

생성된 KeyPath 를 값을 얻는데만 사용하고 싶으시면 변수를 만들 때 타입을 강제시켜주면 됩니다.

let themeKeyPath: KeyPath<BirthdayParty, String> = \BirthdayParty.theme

또한, WritableKeyPath 는 value 타입에만 쓸 수 있습니다.

reference 타입에는 ReferenceWritableKeyPath 를 써야 합니다.


정리하면 다음과 같습니다.

let themeKeyPath: WritableKeyPath<BirthdayParty, String> = \BirthdayParty.theme
let kidAgePath: ReferenceWritableKeyPath<BirthdayParty, Double> = \BirthdayParty.celebrant.age

KeyPath 는 쉽게 사용할 수 있지만, 위와 같이 여러 타입들이 있습니다.

하나하나 다시 살펴보겠습니다.


AnyKeyPath

  • 모든 KeyPath 의 상위 클래스입니다.

PartialKeyPath

  • Root Type 만 표현할 수 있는 타입입니다.

KeyPath<Base, Property>

  • read-only 접근만 가능합니다.

WritableKeyPath<Base, Property>

  • 데이터 수정 권한이 있습니다. 대상이 되는 인스턴스가 value(struct로 만들어진) 타입이어야 합니다.

ReferenceWritableKeyPath<Base, Property>

  • 데이터 수정 권한이 있습니다. 대상이 되는 인스턴스가 reference(class로 만들어진) 타입이어야 합니다.
public class AnyKeyPath : Hashable {
    public static var rootType: Any.Type { get }
    public static var valueType: Any.Type { get }

    final public var hashValue: Int { get }
    public static func ==(a: AnyKeyPath, b: AnyKeyPath) -> Bool
}

public class PartialKeyPath<Root> : AnyKeyPath {
}

public class KeyPath<Root, Value> : PartialKeyPath<Root> {
}

public class WritableKeyPath<Root, Value> : KeyPath<Root, Value> {
}

public class ReferenceWritableKeyPath<Root, Value> : WritableKeyPath<Root, Value> {
}

Key Value Observing

새로운 KeyPath 가 만들어짐에 따라 KVO 의 사용도 매우 쉽게 바뀌었습니다.

KVO 는 말 그대로 Property의 값이 변화하는지 모니터링하는 기능입니다.

외부 요인으로 특정 값이 바뀔 때 처리할 일이 있다면 매우 유용하게 사용할 수 있습니다.

observe 를 하는 방법은 위와 같이 간단합니다.

위의 코드는 mia 라는 인스턴스의 age 프로퍼티를 모니터링하게 됩니다.

age 값이 변화되었음이 감지되면 연결된 closure 함수가 실행됩니다.

observed로 표시된 첫번째 파라미터는 observe를 등록한 인스턴스를 반환합니다. mia 동일한 값입니다. closure 안에서 mia를 다시 호출할 경우 실수로 retain cycle을 발생시킬 확률이 높기 때문에 직접 파라미터로 제공하고 있습니다.

change 는 상세정보를 제공합니다. newValue, oldValue, 추가된 데이터인지, 삭제된 데이터인지 등등의 정보가 담겨 있습니다.

change 에 대한 상세 내용은 문서를 참고해 주세요.

NSKeyValueObservedChange

위의 사용예를 보세요.

KVO에서 하나 주의할 부분은 KeyPath 의 모든 path 요소들이 @objc dynamic 이어야 한다는 점입니다.