코코아바인딩에서 집합 타입의 프로퍼티를 연결할 때 유의할 점

코코아 바인딩을 사용할 때 특정한 키 이름이 변경가능한 배열(NSMutableArray)일 때, UI를 통해 값을 추가/제거하거나 변경한다 하더라도 이러한 변경이 원래 데이터에 반영되지 않는 문제가 발생하는 경우가 있다.

원문 : 코코아 바인딩 문제해결(Troubleshooting Cocoa Bindings)
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CocoaBindings/Concepts/Troubleshooting.html

집합 컨트롤러가 현재 데이터를 표시하지 않아요.

“이러한 문제는 보통 여러분의 애플리케이션이 집합 콘텐츠를 키-밸류 옵저빙 호환 방식으로 데이터를 변경하지 않기 때문에 일어납니다. 배열을 addObject:removeObject: 로 제거하는 것만으로는 부족합니다.”

처방 및 설명

원문에서는 mutableArrayValueForKey:에 답이 있는 것처럼 이야기하는데, 결론적으로는 집합 콘텐츠를 KVO 호환 방식으로 올바르게 조작할 수 있는 방법을 제공해야한다는 말로, 집합에 대한 키밸류 코딩 접근자를 추가해야 한다는 이야기다.

코코아바인딩에 노출된 배열 컨트롤러는 자신의 contentArray로 바인딩되어 있는 배열 내부를 변경하려 할 때, 해당 객체를 직접 조작하지 않는다. 해당 키를 소유한 객체에게 mutableArrayValueForKey:를 호출하여 배열이나 사전등에 대한 집합 프록시(collection proxy)를 얻으려고 시도한다.

이 때 배열 컨트롤러가 얻는 객체는 배열에 대한 프록시로 내부 구조는 배열이 아니더라도 마치 배열처럼 다룰 수 있는 API를 제공해야 한다. 따라서 배열을 기준으로 생각해본다면 프록시는 배열의 크기, 특정 인덱스에서의 요소를 얻을 수 있어야 한다. 또한 배열이 변경가능(mutable)하려면 특정 위치의 값을 다른 값으로 변경하거나 제거하는 수단이 제공되어야 한다. 이러한 기능들을 제공할 의무는 해당 키를 소유한 객체에 있다. 따라서 집합 액세스용 KVC 접근자를 구현해두어야 한다.

배열 컨트롤러가 ArrayValueForKey:MutableArrayValueForKey:를 호출하게 되면 대상 객체에 대해서는 다음과 같은 접근자들을 순서대로 탐색하게 된다.

  1.  countOf<Key> 에 해당하는 접근자를 찾는다.
  2. objectIn<Key>AtIndex: 혹은 <key>AtIndexes: 에 해당하는 접근자를 찾는다.
  3. 만약 위 두 접근자가 발견되면 NSArray의 기능을 대체할 수 있는 배열 프록시를 반환할 수 있다.
  4. 추가적으로 insertObject:in<Key>AtIndex:removeFrom<Key>AtIndex: 가 있다면 NSMutableArray 형태로 리턴되며, 프록시를 통해 원래 배열을 조작할 수 있다.

따라서 배열 컨트롤러를 통해서 arrangedObjects를 변경 조작하려는 작업이 제대로 KVO 호환이 되게 하려면 데이터 집합을 가지고 있는 클래스에서 위의 메소드들을 구현해야 한다.

만약 이러한 구현이 없다면 mutableArrayValueForKey:는 해당 키 이름에 해당하는 NSArray, NSMutableArray 타입의 인스턴스 변수가 있는지 확인하고 해당 키가 가리키는 집합의 사본을 리턴한다. 프록시가 아닌 사본이 리턴되었다면 읽기 액세스에 대해서는 문제가 없지만, 쓰기 액세스는 원본 데이터에 영향을 주지 못하는 문제가 있다.

배열 프로퍼티를 KVO 호환으로 만들지 않는 경우 사본에 대한 변경이 올바른 KVO 통지를 전달하지 않는 문제 외에도, 원본이 변경되는 경우에도 변경된 지점의 정보 없이 전체 배열이 매번 새로 복사되기 때문에 심각한 성능 저하를 가져올 수 있다.

추가 – 코코아 바인딩과 관련한 Swift 이름 문제

Swift로 작성하는 클래스가 KVO를 따르도록할 때 유념해야 할 것은, KVO는 Objective-C 런타임 내에서 돌아가는 레이어 중 일부라는 점이다. 즉 KVO가 필요로 하는 모든 것들 – 클래스와 프로퍼티- 의 이름은 Objective-C 런타임에서 접근이 가능해야 한다.

따라서 클래스 이름 뿐만 아니라 그 내부 프로퍼티/메소드 등 필요한 모든 접근자에 @objc를 붙여야 한다. 또한 함수의 경우, 인자가 2개 이상인 경우에는 첫번째 인자를 생략하는 방식으로 이름이 해석되고 있으므로 (pick(item:at:) 과 같은 이름은 올바르게 변환되지 않는다) 이에 유의해서 작성하거나, 아예 Objective-C 이름을 붙여서 만들어야 한다.

예를 들어 employees 라는 배열 타입 프로퍼티를 가지고 있다면 insertObject:inEmployeesAtIndex:는 다음의 둘 중 하나의 형태로 작성되어야 한다.

@objc func insertObject(_ obj:Employee, inEmployeesAtIndex index: Int) 

// 혹은 objc에서는 별개의 이름을 명시한다.
@objc(insertObject:inEmployeesAtIndex:)
func insert(_ object:Employee, at index:Int)