iOS) Core Data custom class

3 minute read

Core Data custom class 를 설정해보고 relationship 을 활용해서 관계형 데이터베이스화 해보자.

미리 밝힌다. 관계형 데이터베이스를 이용해서 구성하는 것이 올바른 설계지만 커스텀 클래스를 써보고자 했기 때문에 사용해보았다. 결국 릴레이션쉽을 설정해주었다.

Core Data custom class

상황

각 이야기에 해당하는 글이 존재하고 pageIndex 에 따라서 뷰컨에 이야기와 해당 글을 뿌려주고 싶었다…

위와 같이 entity 를 두개를 만들고 StroyListstory attribute 에 custom class 로 StoryModel 배열을 넣어주었다.

  • StoryModel : 타이틀, 디테일, 날짜 정보를 가지는 “이야기” 하나에 들어가는 “글” 이다.
  • StoryList : StoryModel 의 배열(=글 목록)과 “이야기” 의 제목과 소제목 정보를 가짐.

(PenCake 는 이야기에 글들을 등록할 수 있는 앱)

사실 진짜 Core Data 시작과 지금 진행이 이게 돼..?? 의 연속이기때문에 그냥 간과하고 프로젝트를 진행했고 이야기 한개에 글들을 등록하는 StroyModel 만 사용했던 체제에서 여러 스토리를 다루는 StoryList 를 적용하니까 다음과 같은 오류가 나왔다.

error

스크린샷 2021-06-11 오후 3 10 10

iOS 13부터 Apple이 사용자 지정 Core Data 데이터 유형에 대해 NSSecureCoding (NSCoding 대신)을 사용하여 보안을 강화하기를 원했고 내가 설정한 custom class 의 자료형은 NSSecureCoding 이 아니기 때문이다.

그래서 StoryModel 역할을 해줄 커스텀 클래스를 NSSecureCoding 을 준수하도록 만들어서 적용시키기로 했다. 그리고 더이상 StroyModel 은 그 자체만으로써 저장하지 않고 StoryList 에서 저장을 하기 때문에 삭제하기로 했다.

Writings 라는 배열 Writing 를 가지고 있는 커스텀 클래스를 만들어주기로 했다.

해결

첫번째, custom class 에 NSSecureCoding 채택.

스크린샷 2021-06-11 오후 1 22 54

Writing.swift

//
//  Writing.swift
//  PenCake-iOS-CloneCoding
//
//  Created by kimhyungyu on 2021/06/11.
//

import Foundation
import UIKit
//1
public class Writing: NSObject, NSSecureCoding {
    public static var supportsSecureCoding = true
    
    var title: String
    var detail: String
    var date: Date
    
    enum Key: String {
        case title = "title"
        case detail = "detail"
        case date = "date"
    }
    
    init(title: String, detail: String, date: Date) {
        self.title = title
        self.detail = detail
        self.date = date
    }
    //2
    public func encode(with coder: NSCoder) {
        coder.encode(title, forKey: Key.title.rawValue)
        coder.encode(detail, forKey: Key.detail.rawValue)
        coder.encode(date, forKey: Key.date.rawValue)
    }
    
    public convenience required init?(coder: NSCoder) {
        //3
        let mTitle = coder.decodeObject(forKey: Key.title.rawValue)
        let mDetail = coder.decodeObject(forKey: Key.detail.rawValue)
        let mDate = coder.decodeObject(forKey: Key.date.rawValue)
        
        self.init(title: mTitle as! String, detail: mDetail as! String, date: mDate as! Date)
    }

}

주석

1: NSSecureCoding 프로토콜 채택

2: 해당 자료형이 NSSecureCoding 프로토콜을 따르지 않기 때문에 NSString으로 변환 한 다음 인코딩한다.

3: Core Data에서 데이터를 검색 할 때 decodeObject (forKey :)를 사용하여 객체를 디코딩한다.

(참고: String,Date 을 사용하는 멤버들이라서 NSSecureCoding, encode 도 하지 않아도 될거같다. 왜냐면 Core Data 에서 String, Date 은 지원하는 자료형이 아닌가! 하지만 혹시 다른 플젝에서 쓸 때는 얘기가 달라지니까 같이 소개한다.)

Writings

//
//  Writings.swift
//  PenCake-iOS-CloneCoding
//
//  Created by kimhyungyu on 2021/06/11.
//

import Foundation
public class Writings: NSObject, NSSecureCoding {
    public static var supportsSecureCoding = true
    
    public var writings: [Writing] = []
    
    enum Key: String {
        case writings = "writings"
    }
    
    init(writings: [Writing]) {
        self.writings = writings
    }
    
    public func encode(with coder: NSCoder) {
        coder.encode(writings, forKey: Key.writings.rawValue)
    }
    
    public convenience required init?(coder: NSCoder) {
        let mWritings = coder.decodeObject(forKey: Key.writings.rawValue)
        
        self.init(writings: mWritings as! [Writing])
    }
}

두번째, StoryListstory attribute 의 NSSecureUnarchiveFromDataTransformer subclass 만들기

//
//  StoryAttributeTransformer.swift
//  PenCake-iOS-CloneCoding
//
//  Created by kimhyungyu on 2021/06/11.
//

import Foundation

class StoryAttributeTransformer: NSSecureUnarchiveFromDataTransformer {
        //1
    override static var allowedTopLevelClasses: [AnyClass] {
        [Writings.self]
    }
    //2
    static func register() {
        let className = String(describing: StoryAttributeTransformer.self)
        let name = NSValueTransformerName(className)
        let transformer = StoryAttributeTransformer()
        
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}
  • 위의 클래스는 NSSecureUnarchiveFromDataTransformer 하위 클래스로 Core Data에서 검색 할 때 NSData의 값을 올바른 데이터 유형으로 변환하는 방법을 알고 있습니다.

주석

1: 여기서는 값 변환 프로세스에서 최상위 클래스이므로 allowedTopLevelClasses 배열에 Writings 클래스를 추가합니다.

2: custom transformer 를 등록하기 위한 메서드.

세번째, AppDelegate.swift 에 custom transformer 를 등록

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
                StoryAttributeTransformer.register()

        return true
    }

error

위 과정을 빼먹으면 아래와 같은 에러를 마주한다.

스크린샷 2021-06-11 오후 3 10 10

네번째, TransformerCustom Class 설정.

스크린샷 2021-06-11 오후 3 03 05

custom class 설정에 내가 만든 custom class 를 넣어보는 과정이 끝났다.

How to save an array of custom data types in Core Data with Transformable and NSSecureCoding in iOS - Lexicon Digital

결론

위와 같은 방법으로 Core Data 에서 기본적으로 사용하지 못하는 UIColor, CGPoint 를 멤버로 가지는 커스텀 클래스를 만들어서 사용할 수 있다.

하지만 CRUD 는…? 기존의 방법은 entity 를 가져와서 그 entity 에 setValue() 로 값을 추가해주는 방식이었는데 나는 지금 entity 안의 Writings 안에 Story 를 CRUD 해야한다.

→ 릴레이션으로 풀어가보자

Relationship

스크린샷 2021-06-12 오전 12 20 43

스크린샷 2021-06-12 오전 12 20 25

_2021-06-12__12 08 05

문제

위의 방식으로 진행시에 NSSet 으로 저장이 되기 때문에 디비를 설정시에 자료형이 set 이라면 복수 값과 순서가 없기 때문에 무리라고 판단하고 여기까지만 진행했다.

Mastering In CoreData (Part 5 Relationship Between Entities in Core Data)

CRUD

릴레이션을 활용한 CRUD.

Mastering In CoreData (Part 6 Relationship Between Entities in Core Data CRUD Operation)

Categories:

Updated: