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

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

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

[ninja_tables id=”9074″]

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

정리

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

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

 

참고자료

[Cocoa] NSValueTransformer

Value Transformer

코코아 바인딩에서 어떤 키패스의 값을 그대로 사용하는 것이 아니라 다른 단위로 변환하는 등의 변화를 주고 사용해야 한다면 중간에 어떤 매개체가 값을 바꿔서 전달해주는 역할을 하도록 해야 한다. 예를 들어 섭씨 온도를 표시하는 텍스트 필드와 화씨 온도로 조절하는 슬라이드가 있고 이 들을 코코아 바인딩으로 구현하고자 한다면, (물론 모델 객체의 두개의 프로퍼티를 각각 만들고 여기에 바인딩해도 되지만) NSValueTransformer를 사용하여 값 변환 장치를 만들어 사용할 수 있다.

사용하는 일이 흔하지는 않지만, 파운데이션 프레임워크는 이런 값 변환기를 몇 개 가지고 있으며 개발자는 NSValueTransformer를 서브클래싱하여 이런 변환기를 만들 수 있다.

[Cocoa] NSValueTransformer 더보기