Wireframe

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

코어데이터 내의 엔티티의 속성은 문자열, 숫자값, 날짜, 바이너리 데이터등의 기본적인 타입을 지정할 수 있다. 하지만 어떤 경우에는 이런 타입 이외의 커스텀 타입을 사용해야 하는 경우가 있을 수 있다. 예를 들어 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의 모양은 다음과 같다.

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


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

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

버튼은 바인딩이 아니라 액션 메시지를 연결한다. EmployeesController의 add: 와 연결해준다.
다음 아래쪽 박스 내부의 UI는 테이블 뷰에서 선택한 데이터의 세부 내용이다. 두 개의 텍스트 필드와 이미지 뷰를 배열 컨트롤러의 “selection”과 연결해주면 된다

끝으로 메뉴의 Save… 항목을 앱 델리게이트의 saveAction: 과 연결해주면 완성이다.
아래 표는 바인딩 전체 정보에 대한 요약이다.
[ninja_tables id=”9074″]

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

정리

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

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

 

참고자료

Exit mobile version