코어데이터의 기본 개념

코어데이터에 대한 개념 정리를 좀 해보자.

코어데이터는 고정된 저장소와 메모리상의 스크래치 보드를 연결하여, 마치 어떤 데이터 클래스 객체들의 배열을 다루는 코드로 데이터를 생성, 편집, 삭제하며 저장할 수 있는 기능을 제공하는 프레임워크이다. 코어 데이터 내에서 사용되는 데이터 클래스들은 각각의 프로퍼티에 대해 확장된 KVC/KVO를 사용하는 것으로 최대한 유연하게 동작하기 때문에 사실상 개별 데이터의 클래스에 구애받지 않는 범용 API들을 설계해두고 있다.

따라서 비록 정확하지는 않지만 코어데이터는 DB액세스를 OOP 형식으로 만들어놓았다고 생각하고 사용할 수 있다. 이 관점에서 바라볼 때 코어데이터의 데이터 모델에는 세 가지의 기본적인 개념이 있다. 그것은 엔티티와 속성(attributes) 그리고 관계(relationships)이다.

엔티티

엔티티는 DB의 테이블 스키마에 해당된다. 엔티티는 고유의 이름으로 다른 엔티티와 구분된다. 엔티티는 그 내부에 속성과 관계를 정의하고 있다. 속성(attributes)은 클래스의 프로퍼티에 해당하며, 해당 엔티티의 레코드가 가지는 속성값을 말한다. 관계(relationships)은 엔티티가 다른 엔티티를 참조하는 것으로 클래스에서는 다른 타입의 객체 혹은 객체 배열에 대한 참조에 해당한다. 즉 엔티티 그 자신은 하나의 테이블인 동시에 클래스 하나로 번역되고 속성과 관계는 모두 클래스 상에서의 프로퍼티에 해당하며, 속성인 경우에는 데이터를 저장하며, 관계인 경우에는 다른 엔티티가 나타내는 클래스에 대한 참조로 번역된다.

엔티티 내의 개별 데이터, DB로 치면 테이블 내의 레코드 1개에 해당하는 값을 관리되는 객체(managed object, 편의상 이후 ‘관리객체’라 하겠다.)라고 한다. 관리 객체의 원형은 NSManagedObject라는 클래스이며, 코어데이터에서는 각 엔티티마다 그 구조를 따라 모델링되는 NSManagedObject 의 서브 클래스를 자동으로 만들어 낸다. 즉, 하나의 레코드 값은 NSManagedObject의 서브 클래스의 인스턴스가 되며, 해당 서브 클래스의 모든 속성과 관계는 코어데이터가 자동으로 생성해준다. 이는 컴파일 타임에 자동으로 생성하는 것도 가능하지만, 커스터마이징을 위해서 미리 생성하여 프로젝트에 파일 형태로 포함할 수도 있다.

관리객체 (Managed Object)

관리객체는 코어데이터에 의해서 관리되는 데이터 객체라는 의미이다.  앱에서 생성되거나 저장소로부터 로드된 데이터는 메모리상에서 이 관리객체 인스턴스가 된다. 관리 객체 인스턴스는 NSManagedObject 클래스의 인스턴스이다. 프로젝트 내에서 각 엔티티마다 NSManagedObject의 서브 클래스를 작성(혹은 Xcode가 생성)하기는 하지만, 이는 사실 특별한 비즈니스 로직이 필요하거나 하는 등의 경우에 필요할 뿐이다. 기본적으로 관리 객체는 자신의 속성에 관심이 없다. 관리 객체는 단지 “하나의 테이블 레코드”를 의미하는 바 그 이상도 이하도 아니며, 여기서 중요한 것은 코어데이터가 관심있어 하는 것은 특정한 레코드가 로딩된 이후에 변경/삭제 되었거나 혹은 새로운 관리 객체가 삽입되었냐 하는 것이다. 물론 하나의 관리 객체 인스턴스는 저장된 값을 액세스할 수 있어야 하며, 이는 클래스의 구조보다는 KVC를 통하여 일종의 사전처럼 속성 이름을 사용해서 그 값을 간접적으로 액세스한다.

따라서 NSManagedObject의 서브 클래스에 의해서 해당 객체의 프로퍼티가 정의된다고 생각하면 안된다. 관리객체의 프로퍼티는 접근자는 물론, 인스턴스 저장공간까지 모두 코어데이터에 의해서 자동으로 관리된다. NSManagedObject의 서브 클래스가 만들어지는 것은 오로지 “코드 상에서 KVC를 통해서 특정한 속성/관계를 쉽게 액세스하기 위해서”일 뿐이다. 관리 객체와 엔티티의 관계에 대해서는 뒤에서 추가적으로 설명하겠다.

모델, 관리되는 객체 모델(Managed Object Model)

관리되는 객체 모델(이후, 편의상 관리객체 모델)은 데이터베이스 전체의 스키마에 해당한다. 엔티티가 단일 데이터의 스키마라고 할 때, 모델은 그 내부의 모든 엔티티와 둘 이상의 테이블간의 관계에 이르는 전체 내용을 포괄한다. 하나의 모델에는 두 개 이상의 엔티티가 있을 수 있고, 하나의 앱은 두 개 이상의 모델을 사용할 수 있다.

관리 객체 모델은 코드상으로 편집하는 것도 가능하지만, 어차피 컨텍스트나 저장소 코디네이터와 연결된 후에는 변경이 불가하므로 이렇게 변경할 일은 거의 없다고 보면 된다. 대신에 거의 대부분의 경우, 관리 객체 모델은 코어데이터 편집기를 통해서 미리 정의해둔 코어데이터 모델 파일(momd)을 읽어들여서 생성하는 방식으로 사용한다.

데이터 베이스가 단순한 테이블의 집합이라면, 모델은 엔티티들의 집합인 동시에 각 엔티티가 어떤 속성을 가지고 있으며, 엔티티끼리는 어떤 관계를 가지고 있는지에 대한 정보를 담고 있다. 즉 각각의 엔티티에 의한 관리객체 내부가 어떻게 생겼는지에 대해서는 코어데이터는 관리객체 클래스보다는 이 모델을 통해서 파악하게 된다.

영구 저장소 코디네이터 (Persistent Store Coordinator)

영구 저장소 코디네이터는 (이하 줄여서 코디네이터라고 부른다.) 데이터베이스에 대한 I/O를 담당하는 부분이다. 코디네이터는 이후 기술할 컨텍스트의 요청을 받아 필요한 데이터를 읽고 조회하여 올려주고, 컨텍스트가 내려준 변경사항을 파일에 저장한다. 영구저장소로는 XML 파일이나 SQLite 데이터 베이스를 사용할 수 있으며, 하나의 코디네이터는 한 개 이상의 저장소를 동시에 관리할 수 있다. (예전에는 이런 방식으로 입출력 속도를 높였는데, 지금은 입출력과 관련한 병렬처리 지원이 되어서 1개로도 충분!)

영구 저장소 코디네이터는 사실 단순 입출력을 담당하고 있으며, 실제 저장소 파일이나 DB의 구조에 대해서는 스스로 처리하지 못한다. 이러한 부분은 전적으로 관리 객체 모델에 의존해야 하며, 따라서 코디네이터를 생성하기 위해서는 관리객체모델이 필수적으로 필요하다.

컨텍스트 – 관리 객체 컨텍스트 (Managed Object Context)

관리 객체 컨텍스트는 (이하 줄여서 컨텍스트라고 부른다.) 일종의 코르크보드 같은 게시판으로 생각할 수 있다. 그리고 코어데이터에서 가장 중요하게 여겨지는 중심 객체이다. 코어데이터에서 모든 데이터는 이 컨텍스트 상에서 관리 객체로 생성되거나, 저장소로부터 로딩해와서 올려놓을 수 있다. 컨텍스트 상의 모든 관리 객체의 생성, 제거, 변경은 컨텍스트에 의해서 모두 추적/관리된다.

컨텍스트는 현재 상태의 변경 사항을 기록해두기 위해서 영구 저장소 코디네이터를 필요로 한다.

영구 저장소 컨테이너 (Persistent Container)

Xcode8에서부터 NSPersistentContainer가 새롭게 추가되었다. 이전까지의 코어데이터 스택을 구성하는 코드들은 사실상 코어데이터 모델 파일의 이름만 다를 뿐, 거의 모든 내용이 동일했기 때문에, 스택 셋업을 편리하게 할 수 있는 요건을 제공한다. 또한, 컨테이너를 사용하는 경우, viewContext라는 프로퍼티를 통해서 컨텍스트 역시 이미 셋업된 상태로 얻을 수 있다. 그외에 몇 가지 추가적인 편의 메소드들을 제공해주고 있다.

정리

이상으로 코어데이터에서 사용되는 개념 및 주요 클래스들을 살펴보았다.  이들에 대한 내용을 핵심만 간단하게 다시 추려보자면 다음과 같다.

  1. 데이터를 조회하거나 삽입, 삭제하는 것은 컨텍스트를 사용해서 처리한다.
  2. 개별 데이터는 NSManagedObject의 (혹은 그 서브 클래스의) 인스턴스로 표현된다. 이 때 서브클래스는 코드상에서 개별 데이터 내의 속성 및 관계을 쉽게 액세스하기 위해서 필요하며, 실제 데이터 액세스 과정은 코어데이터 런타임에 의해서 KVC를 통해 이뤄진다.
  3. 개별 데이터의 실제 구성은 엔티티에 의해 정의된다. 엔티티는 DB의 테이블 스키마에 해당되는 개념이며, 코드상에서는 이름으로만 노출된다.
  4. 엔티티들의 집합은 관리객체모델이다. 모델은 코어데이터 모델편집기를 통해서 그 내부에 들어갈 엔티티들을 정의하게 되고, 이는 .momd 패키지로 앱번들에 포함된다. 모델은 일단 코어데이터 모델 편집기를 통해서 생성한 후에는 코어데이터 내부 알고리듬에 의해 참조되며, 개발자가 실제로 모델을 조작할 일은 거의 없다.
  5. 컨텍스트에서 데이터를 추가/변경한 내용을 실제로 디스크에 입출력하는 부분은 저장소 코디네이터에 의해 수행된다. 코어데이터 API의 상부는 데이터 모델에 별 관심이 없으며, 오직 저장소 코디네이터가 모델을 참조하여 데이터를 올바른 위치에 쓰거나, 읽게 된다.

결국 코어데이터를 앱에 적용하기 위해서는 Xcode 프로젝트에서 코어데이터 모델 편집기를 통해서 모델 파일을 생성하고, 모델 파일의 이름으로부터 코어데이터 스택을 수동으로 생성/초기화하거나 컨테이터를 생성하여 코어데이터를 활성화할 수 있다. 코어데이터 스택이 만들어진 다음부터는 컨텍스트를 이용해서 저장된 데이터를 fetch하거나 신규로 생성된 데이터를 삽입할 수 있다. 이후의 디스크 입출력과 관련된 부분은 코어데이터 스택이 알아서 관리하며 (명시적으로 컨텍스트의 변경사항을 저장하는 것은 가능하다.) 추가적인 몇 가지 컨트롤러 클래스등과 연계한다면 작성해야 하는 코드를 비약적으로 줄일 수 있다.

예컨데, macOS용 앱을 만드는 경우에 문서기반앱으로 코어데이터를 포함하여 프로젝트를 시작하고 UI와 접합점에 코코아 바인딩을 적용하는 경우, 간단한 데이터 관리 앱을 코드 1줄 쓰지 않고 만드는 것도 가능하다. 코어데이터가 가장 좋은 DB추상화 레이어라고 말할 수는 없지만 애플의 다른 기술들과 연계하였을 때에는 매우 강력한 도구로 쓰일 수 있을 것이다.

참고자료

NSPersistentContainer를 통한 코어데이터 스택생성하기

macOS Sierra로 업데이트되면서 코어데이터에 NSPersistentContainer 클래스가 추가되었다. 이 클래스를 사용하면 코어데이터 스택을 셋업하는 여러 귀찮은 과정을 생략하고 간단하게 처리할 수 있다. 사실 코어데이터 스택을 수동으로 셋업하는 과정에서 필요한 정보는 코어데이터 모델 파일의 이름과, 저장소 파일을 생성할 위치 정도이며, 그외의 대부분의 코드는 보일러 플레이트라 할 수 있다.  저장소 파일 위치는 적당한이름(?)으로 사용자 라이브러리 내에 만들어지므로 결국 최소한으로 필요한 정보는 데이터 모델 파일 이름이 된다.

수동 셋업 과정은 다음과 같다.

/// Objective-C
@interface AppController: NSObject
@property (readonly, strong) NSPersistentContainer* persistentContainer;
@end

@implmentation AppController
@synthesize persistentConatainer=_persistentContainer;

- (NSPersistentContainer*)persistentContainer
{
  if(!_persistentContainer) {
    _persistentContainer = [[NSPersistentContainer alloc]
                            initWithName: @"MyDataModel"]; // 이름에 확장자는 붙이지 않는다.
    [_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *desc, NSError *error){
       if(!error) {
          /// 저장소 로딩 중 에러가 발생
          NSLog(@"Unresolved error %@, %@", error, error.userInfo);
       }
    }];
  }
  return _persistentContainer;
}

매우 간단하게 모든 설정이 완료됐다. 관리객체모델과 컨텍스트는 managedObjectModel, viewContext 프로퍼티로 액세스할 수 있다. 컨텍스트만 따로 외부로 노출하려한다면, 다음과 같이 프로퍼티를 선언한다.

@property (readonly, nonatomic) NSManagedObjectContext* context;
....
- (NSManagedObjectContext*) context { return self.persistentContainer.viewContext }

저장소 디스크립션을 사용하기

NSPersistentStoreCoordinator를 직접 셋업하는 경우에는 저장소 위치와 타입등의 정보를 설정해야 했다. 이러한 설정정보를 하나의 클래스로 묶은 것이 저장소 디스크립션으로 NSPersistentStoreDescription이라는 클래스로 만들어져있다. NSPersistentContainer는 이 디스크립션을 이용해서 “각각의 저장소들을” 생성한다. 기본적으로 아무런 정보가 주어지지 않으면 컨테이너는 SQLite 타입의 저장소를 라이브러리 디렉토리 내에 저장하게 된다. 만약 저장소 위치를 옮기고 싶거나, 타입을 바꾸고 싶으면 새로운 저장소 디스크립션을 생성해서 바꿔주면 된다. 대신 이 동작이 유효하려면 loadPersistentStores...:를 호출하기 전에 설정을 변경해두어야 한다.

/// 위치를 사용자 문서 폴더로 바꾸고 싶을 때
  NSURL* dirURL = [[[NSFileManager defaultManager] 
                       URLsForDirectory:NSDocumentDirectory
                       inDomains:NSUserDomainMasks] lastObject];
  NSURL* storeURL = [dirURL URLByAppendingPahtComponent:@"mydata.db"];
  NSPersistentStoreDescription* desc = [[NSPersistentStoreDescription alloc] initWithURL:storeURL];
  desc.type = NSSQLiteStoreType;
  [_persistentContainer setPersistentStoreDescriptions:@[desc]];
  [_persistentContainer loadPersistentStoresWithHandler:^ ...

NSPersistentStoreCoordinator를 사용하는 경우에도 -addPersistentStoreWithDescription:completionHandler:를 사용할 수 있으니 참고하자.

Swift 버전 코드

같은 내용인데, Swift 버전의 코드는 아래와 같다.

lazy var container: NSPersistentContainer = {
  let container = NSPersistentContainer(name:"MyDataModel")
  contaner.loadPersistentStore{ desc, error in 
    if error {
      fatalError("Fail to load : \(error)")
    }
}()

var context: NSManagedObjectContext {
  return self.container.viewContext
}

조금 더 깊이

Swift에서 init(name:) 은 편의 이니셜라이저이다. 만약 이 컨테이너를 서브 클래싱할 때 편의 이니셜라이저를 만드려면 지정 이니셜라이저를 호출해야한다. 컨테이너의 지정 이니셜라이저는 init(name:managedObjectModel:) 이므로 전달된 이름을 가지고 관리 객체 모델을 구해서 이를 호출해야 한다. 관리 객체 모델은 수동 셋업때와 같이 모델 파일로부터 로딩해서 생성하면 된다.

convenience init(completionHandler: @escaping () -> ()) {
  guard let mURL = Bundle.main.url(forResource:"MyDataModel", withExtension:"momd")
  else {
    fatalError("Can't load model from bundle.")
  }

  guard let mom = NSManagedObjectModel(contensOf:mURL) else { 
    fatalError("Error initializing MOM")
  }
  init(name:"MyDataModel", managedObjectModel:mom)
  completionHadler()
}

참고 자료

코어데이터에서 커스텀 타입 속성을 사용하기

코어데이터 내의 엔티티의 속성은 문자열, 숫자값, 날짜, 바이너리 데이터등의 기본적인 타입을 지정할 수 있다. 하지만 어떤 경우에는 이런 타입 이외의 커스텀 타입을 사용해야 하는 경우가 있을 수 있다. 예를 들어 NSColor라든지, 혹은 CGRect, CGPoint, CGSize와 같은 C 구조체로 된 정보 또, 아예 직접 작성한 커스텀 타입인 경우도 있을 수 있다. 이러한 커스텀 타입을 엔티티의 속성으로 사용하는 방법에 대해서 알아보자.

코어데이터 모델 편집기에서 엔티티 내 속성(attribute)의 타입을 선택하기 위한 팝업 메뉴 중에는 Transformable 이라는 타입이 존재한다. 이 타입은 저장소에 저장될 때는 바이너리 데이터로 저장되지만 컨텍스트로 올라갈 때는 다른 형식으로 변환되어 올라가도록 동작하게 된다.

속성의 타입을 Transformable로 선택한 후 오른쪽 데이터 모델 인스펙터를 보면 그림과 같이 Value Transformer를 설정하거나 Custom Class를 설정하는 부분이 있다. 그림의 예는 NSColor를 코어데이터에 저장하기 위한 설정을 보여주고 있다.

커스텀 클래스에 NSColor를 기입하고, Value Transformer는 비워둔다. 이 상태로 해당 엔티티에 대한 클래스를 생성하면, NSColor 타입의 프로퍼티가 생성되어 있는 것을 확인할 수 있다. (만약 커스텀 클래스 란을 비워두면 타입이 id로 정의된다.)

이때, NSColorNSCoding을 따르는 클래스이다. 따라서 별도의 트랜스포머 없이 데이터와 컬러 객체간의 변경이 가능하다. 즉, 커스텀 클래스를 만들어서 이 타입을 엔티티 속성에 사용한다고 하면, 해당 클래스가 NSCoding을 준수하도록만 해주고 모델 편집기에서는 Transformable로 타입을 설정해주면 된다.

NSCoding을 따르지 않는 클래스를 사용하는 방법으로는 트랜스포머를 사용하는 방법이 있다. 예를 들면 NSImage 같은 것을 코어데이터에 어떻게 저장할 것인가 하는 부분이다. (코코아 바인딩을 사용한다면 이미지뷰나 이미지웰의 data 항목을 사용하면 굳이 NSImage 인스턴스가 아닌 NSData만으로도 구현은 가능하다.)

NSImageNSCoding을 따르지 않으므로 트랜스포머를 만들어야 한다. 이 때 주의할점은 앱 -> 코어데이터로의 방향이 정방향이라는 점이며, 따라서 transformedValueClass는 항상 NSData여야 한다는 점이다.

/// PhotoTransformer for NSImage into Core Data

@interface PhotoTransformer: NSValueTransformer
@end

@implementation PhotoTransformer

+ (Class)transformedValueClass {
  return [NSData class];
}

+ (BOOL)allowsReverseTransformation
{
  return YES;
}

- (id)transformedValue:(id)value
{
  // NSImage -> NSData 변환
  NSImage* image = (NSImage*)value;
  return [image TIFFRepresentation];
}

- (id)reverseTransformedValue:(id)value
{
  NSData* data = (NSData*)value;
  NSImage* image = [[NSImage alloc] initWithData:data];
  return image;
}

// 트랜스포머를 등록하기. 이 부분은 여기보다는 앱 델리게이트에서 해주는 것이 좋다.
+ (void)initialize
{ 
  PhotoTransformer *pt = [[PhotoTransformer alloc] init];
  [NSValueTransformer setValueTransformer:pt
                      forName:@"PhotoTransformer"];
}
@end

Swift 버전

일전에 값 트랜스포머 관련해서 포스팅을 한 번 했을 때에도 언급했던 것 같은데, Swift를 써서 값 트랜스포머를 쓸 때에는 몇 가지 다른 점이 있다.

  1. Swift에서 ValueTransformer 클래스는 NSObject의 서브 클래스가 아니다. 따라서 initialize를 오버라이딩하는 부분을 만들 수 없다. 따라서 이 부분은 앱 델리게이트의 메소드를 오버라이딩해서 호출되도록 해야 한다.
  2. ValueTransformer의 이름은 단순 문자열 타입이 아니라 NSValueTransformerName 이라는 별도 타입으로 정의된다.  보통 서브클래스를 만든 후에 ValueTranformer를 확장하여 해당이름을 클래스 속성으로 추가한다.

동일한 클래스를 Swift로 작성하면 다음과 같다.

@objc(PhotoTransformer)
class PhotoTransformer: ValueTransformer {
  override class func transformedValueClass() -> AnyClass { return NSData.self }
  override class func allowsReverseTransformation() -> Bool { return true }

  override func transformedValue(_ value: Any?) -> Any? {
    // 
    if let image = value as? NSImage {
      return image.tiffRepresentation
    }
    return nil;
  }
  
  override func reverseTransformedValue(_ value: Any?) -> Any? {
    if let data = value as? Data {
      return NSImage(data: data)
    }
    return nil
  }
}

// 이름을 추가 등록
extension ValueTransformer {
  static let photoTransformerName = NSValueTransformerName(rawValue: "PhotoTransformer")
}

샘플 프로젝트

간단한 실증용 프로젝트를 만들어보자. (Objective-C로 진행했다.) 프로젝트를 하나 생성한다. 시작 시 코어데이터에 체크하여 코어데이터 스택이 미리 준비되도록 한다.

가장 먼저할 일은 코어데이터 모델 파일을 열어서 엔티티를 추가하는 것이다. 다음과 같이 간단하게 3개의 속성만을 정의한다. photo는 사진 속성으로 Transformable 타입으로 선택한다.

photo 속성을 선택하고 데이터 모델 인스펙터를 통해서 몇 가지 세부 사항을 정의한다.  커스텀 클래스는 NSImage로 기입하고, Value Transformer Name에는 “Photo2DataTransformer”라고 쓴다. 값 트랜스포머를 만든 후에 반드시 이 이름으로 등록해야한다

데이터 모델에 대한 편집은 이것으로 끝났다. 다음은 Photo2DataTransformer를 작성할 시간이다. 새 파일을 추가하고 Cocoa Class를 선택한다. NSValueTransformer의 서브 클래스를 만든다고 설정하면 헤더에 Foundation.h를 임포트하게 되는데, 이 상황에서는 NSImage가 노출되지 않으니, 이 부분을 <Cocoa/Cocoa.h>로만 변경해주면 된다. 이후 소스는 위에서 설명한 내용과 동일하다.

다음은 등록을 위해서 앱 델리게이트를 편집할 차례이다. 앱 델리게이트에서는 두 가지 작업을 처리해야 한다.

  1. UI는 코코아 바인딩으로 설정할 것이므로 NSManagedObjectContext에 접근할 수 있는 접근자를 준비한다.
  2. 앞서 작성한 Photo2DataTransformer를 등록한다.

 

/// in AppDelegate.m

#import "Photo2DataTransformer.h"  // 1

@interface AppDelegate ()
...
- (NSManagedObjectContext *)context; // 2
@end

@implementation AppDelgate
...

+ (void)initialize //3
{
  [super initialize];
  Photo2DataTransformer *pt = [[Photo2DataTransformer alloc] init];
  [NSValueTransformer setValueTransformer:pt
                      forName: @"Photo2DataTransformer"];
}

- (NSManagedObjectContext*)context // 4
{
  return self.persistentContainer.viewContext;
}
...
  1. Photo2DataTransformer.h 헤더 반입
  2. 컨텍스트 접근자 선언
  3. 값 트랜스포머를 등록한다.
  4. 컨텍스트 접근자 구현

모든 코드 작업은 끝났고, 이제 UI를 만들 차례이다. 만들어질 UI의 모양은 다음과 같다.

  • 테이블 뷰는 뷰 기반 테이블 뷰로 1개 칼럼을 가진다.
  • 테이블 뷰 셀 뷰 내에는 1개의 이미지 뷰와 2 개의 텍스트레이블을 위치시켰다.
  • Add 버튼을 하나 추가한다.
  • Box를 하나 추가하고 그 속에, 두 개의 텍스트레이블+필드 쌍과 이미지웰(ImageWell)을 추가했다.
  • 참고로 ImageWell은 선택해서 Editable 속성에 체크해주어야 한다.

그리고 ArrayController 하나를 추가한다. 이름을 EmplyeesController라고 짓고, 다음과 같이 엔티티 모드로 하고 엔티티 이름을 준다. 그리고 앱이 시작했을 때 저장된 내용을 읽어와 보여주도록하려면 Prepares Content에도 체크한다.

다음은 테이블 뷰를 선택해서 바인딩 Table Content를 배열컨트롤러의 컨트롤러 키 “arrangedObject”에 바인딩한다.  그리고 테이블 뷰의 선택한 행과 배열컨트롤러의 선택된 값을 동기화하기 위해서 테이블 뷰의 바인딩 Selection Indexes를 배열 컨트롤러의 컨트롤러 키 “selectionIndexes”와 바인딩한다.

  • 테이블 뷰 바인딩 설정
    • 바인딩: Table Content
      • 대상 : EmployeesController
      • 컨트롤러 키 : arrangedObjects
    • 바인딩: Selection Indexes
      • 대상: EmplyeesController
      • 컨트롤러 키 : selectionIndexes

다음은 테이블 뷰 셀 내부의 뷰들에 대해 바인딩한다. 테이블 뷰 셀 내부의 컨트롤들은 모두 테이블 뷰 셀과 바인딩하면서 셀의 “objectValue”의 하위 키패스와 바인딩하면 된다.

  • 이미지 뷰 (테이블 뷰 셀 내) 바인딩
    • 바인딩: Value (주의: Data가 아님)
      • 대상 : Table Cell View
      • 모델 키 : objectValue.photo
  • 텍스트 필드 두 개
    • 바인딩 : value
      • 대상 : Table Cell View
      • 모델 키 : objectValue.firstName
        (아래쪽 텍스트 필드는 모델 키만 objectValue.lastName을 쓰고 나머지는 동일하다.)

버튼은 바인딩이 아니라 액션 메시지를 연결한다. EmployeesController의 add: 와 연결해준다.

다음 아래쪽 박스 내부의 UI는 테이블 뷰에서 선택한 데이터의 세부 내용이다. 두 개의 텍스트 필드와 이미지 뷰를 배열 컨트롤러의 “selection”과 연결해주면 된다

  • 이미지 뷰 (Image Well)
    • 바인딩: Value
      • 대상 : EmployeesController
      • 컨트롤러 키 : selection
      • 모델 키 : photo
  • 텍스트 필드 2 개
    • 바인딩 : value
      • 대상 : EmployeesController
      • 컨트롤러 키 : selection
      • 모델 키 : firstName, lastName

끝으로 메뉴의 Save… 항목을 앱 델리게이트의 saveAction: 과 연결해주면 완성이다.

아래 표는 바인딩 전체 정보에 대한 요약이다.

샘플 프로젝트 다운로드 : https://app.box.com/s/r5arwd1g4uhq1wd3z9p5elexli7gpun1

정리

코어데이터에서 기본적으로 지원되지 않는 타입을 사용하는 경우 다음의 방법을 쓰면 된다.

  1. 커스텀 클래스가 NSCoding을 지원하면 해당 속성을 Transformable로 설정하면 된다.
  2. 커스텀 클래스가 NSCoding을 지원하지 못하는 경우, Transformable 타입으로 설정하고 값 트랜스포머를 만들어서 설정해준다.

 

참고자료