# 코어데이터 프로젝트 생성

코어데이터 기반 앱 제작을 위해 프로젝트 생성 시 옵션을 체크하면 자동으로 생성되는 파일 몇 가지가 있다. .xcdatamodeld, Persistence.swift 파일이 기본적으로 만들어지고 이니셜 뷰로 사용되는 @main에 메인 컨텍스트가 environment로 주입된다.

@main
struct swiftUIMemoApp: App {
    @StateObject var store = MemoStore()
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            MainListView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
                .environmentObject(store)
        }
    }
}

# PersistenceController.swift

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "SwiftUITest")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

위 코드는 프로젝트 생성 시 자동으로 작성되는 코어데이터 코드 스니펫의 일부인데, 이니셜라이저 부분이 중요하다. inMemory 파라미터는 디스크가 아닌 메모리에 변경사항이 저장되는지 여부를 판단하게 되며 메모리에 저장되는 경우 앱 종료와 함께 변경된 컨텍스트가 업데이트 되지 않기 때문에 처리해둔 코드인 것이다.

PersistenceController 구조체를 코어데이터 싱글톤 매니저 클래스로 정의해보면 아래와 같게 된다.

class CoreDataManager: ObservableObject {

    static let shared = CoreDataManager()

    let container: NSPersistentContainer

    var mainContext: NSManagedObjectContext {
        return container.viewContext
    }

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "swiftUIMemo")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {

                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

이니셜라이저와 컨테이너만 그대로 가져오고 mainContext 속성을 추가하여 CRUD 로직을 구현한다. 데이터 변경이 감지되면 컨텍스트의 hasChanges 속성이 true로 변경되고 이에 따라 분기처리 하여 코어데이터로의 데이터 업데이트를 진행하면 된다.

import Foundation
import CoreData

@objc(MemoEntity)
public class MemoEntity: NSManagedObject {

}

우선 위와 같이 코어데이터 데이터모델을 기준으로 엔티티를 생성한다.

func saveContext() {
    if mainContext.hasChanges {
        do {
            try mainContext.save()
        } catch {
            print(error)
        }
    }
}

func addMemo(content: String) {
    let newMemo = MemoEntity(context: mainContext)
    newMemo.content = content
    newMemo.insertDate = Date.now

    saveContext()
}

위와 같이 새로운 메모 엔티티 추가 후 변경된 컨텍스트를 저장하는 식으로 코어데이터에 접근하면 된다. 커스텀 싱글톤 클래스를 정의했으므로 코어데이터 컨텍스트를 엔트리 포인트 뷰에서 environment 모디파이어에 전달하여 하위 뷰에서 접근 가능하도록 하면 셋업은 마무리된다.

@main
struct swiftUIMemoApp: App {
    @StateObject var store = MemoStore()
    let manager = CoreDataManager.shared

    var body: some Scene {
        WindowGroup {
            MainListView()
                .environment(\.managedObjectContext, manager.mainContext)
                .environmentObject(store)
        }
    }
}

# @FetchRequest

@FetchRequest 프로퍼티 래퍼를 사용하면 코어데이터로부터 데이터 요청이 더 간단해진다. environment 모디파이어를 통해 상위 뷰로부터 코어데이터 컨텍스트를 전달받고 있다는 가정 하에 하위 뷰에서 사용 가능한 프로퍼티 래퍼이다. @FetchRequest는 뷰 속성에 대해 코어데이터로의 데이터 요청 및 생성 요청을 자동적으로 해주도록 한다.

코어데이터 fetch 요청은 쿼리하고 싶은 엔티티에 대한 정보와 요청한 데이터의 정렬 기준인 sort descriptor 두 가지 정보를 필요로 한다.

@FetchRequest(sortDescriptors: [SortDescriptor(\MemoEntity.date, order: .reverse)])
var memoList: FetchedResults<MemoEntity>

키패스 문법을 통해 쿼리할 엔티티 클래스 속성을 참조하는 형태이다. 배열 데이터이므로 요청 완료된 데이터를 리스트에 바인딩해주면 뷰 구성이 쉽게 된다.

List(memoList) { memo in
    Text(memo.content)
}

NSPredicate

sortDescriptor가 데이터 정렬 기준이라면 NSPredicate은 특정 데이터를 뽑아올 때 사용한다.

NSPredicate(format: "name == %@", "Python")

@FetchRequest프로퍼티 래퍼 이니셜라이저에서 predicate 파라미터에 위 객체를 전달한다.

@FetchRequest(
    sortDescriptors: [SortDescriptor(\.name)],
    predicate: NSPredicate(format: "name == %@", "Python")
) var languages: FetchedResults<ProgrammingLanguage>

주의사항

@FetchRequest 프로퍼티 래퍼는 반드시 뷰 안에서만 사용해야 한다.

# Reference

  1. Hacking With Swift - How to configure Core Data to work with SwiftUI (opens new window)
  2. Hacking with Swift - How to create a Core Data fetch request using @FetchRequest (opens new window)