Core Data는 기기에서 데이터를 유지하거나 캐시하거나, CloudKit으로 여러 장치에 데이터를 동기화할 수 있게 합니다.
Archiving은 객체를 저장할 뿐만 아니라 객체 간의 연결된 관계도 저장하는데 객체간의 관계를 묶어주는 것을 Object Graph 라고 합니다. Archiving은 Object Graph를 저장하기 좋은 기능입니다, 하지만 제한적입니다. Object Graph를 Archiving으로 저장하는 것은 확장하거나 수정하기 어렵습니다. 왜냐면 전체 Graph는 약간의 수정이 있다고 해도 반드시 파일에 저장되어야 하기 때문입니다. 또한 어떤 객체가 저장될지 객체간의 연결을 컨트롤하기가 쉽지 않습니다.
이런 Archiving의 단점을 보강해주기 위해서 Core Data를 사용합니다. Core Data는 Object Graph manager로 객체와 연결을 직접 정의하고 데이터베이스에 저장하여 관리합니다. Core Data를 사용하면 객체간의 relationship을 결정할 수 있으며, 시스템이 객체를 encoding, decoding하며 일관성을 유지하고 효율성을 극대화합니다.
Overview
Core Data를 사용하여 오프라인 사용을 위해 애플리케이션의 영구 데이터를 저장하고, 임시 데이터를 캐시하고, 기기에서 앱에 실행 취소 기능을 추가할 수 있습니다. 단일 iCloud 계정의 여러 장치에서 데이터를 동기화하기 위해, Core Data는 자동으로 스키마를 CloudKit 컨테이너에 미러링합니다.
추가로 Core Data의 Model 편집기를 통해 데이터 타입과 관계를 정의하고 각 클래스 정의를 생성합니다. 그런 다음 코어 데이터는 런타임에 객체 인스턴스를 관리하여 다음과 같은 기능을 제공할 수 있습니다.
Persistence
Core Data는 객체를 저장소에 매핑하는 세부사항을 추상화하며, Swift 및 Objective-C에서 데이터를 직접적으로 데이터베이스를 관리하지 않고 쉽게 저장할 수 있게 해줍니다.
Undo and redo of individual and batched changes
Core Data의 되돌리기(undo) 관리자는 변경 사항을 추적하고 이를 개별적으로, 그룹으로 또는 한꺼번에 되돌릴 수 있으며, 앱에 되돌리기 및 다시 실행 지원을 쉽게 추가할 수 있도록 해줍니다.
Background data tasks
UI를 블록할 수 있는 데이터 작업(예: JSON을 객체로 파싱)을 백그라운드에서 수행할 수 있습니다. 그런 다음 결과를 캐시하거나 저장하여 서버와의 라운드트립을 줄일 수 있습니다.
View Synchronization
Core Data는 또한 테이블과 컬렉션 뷰에 대한 데이터 소스를 제공하여 뷰와 데이터를 동기화하는 데 도움을 줍니다..
Versioning and migration
Core Data는 데이터 모델의 버전 관리 및 앱이 진화함에 따라 사용자 데이터를 이동하는 메커니즘을 포함하고 있습니다.
Creating a Core Data Model
앱의 객체 구조를 data model 파일로 만듭니다.
Overview
Core Data 작업의 첫 번째 단계는 데이터 모델 파일을 만들어 앱의 객체 구조를 정의하는 것입니다. 이는 객체의 유형, 속성 및 관계를 포함합니다.
Core Data model 파일을 Xcode 프로젝트를 만들때 또는 존재하는 프로젝트에 추가할 수 있습니다.
Add Core Data to a New Xcode Project
새로운 프로젝트를 만드는 대화상자에서 Use Core Data 체크박스를 클릭합니다.
새롭게 만들어진 프로젝트는 .xcdatamodeld 파일을 포함하고 있습니다.
Add a Core Data Model to an Existing Project
Core Data의 Object Graph는 data model 로 정의되어 있으며 이 data model은 MVC 패턴과는 관련 없습니다. Core Data model은 graph가 entity라고 불리는 객체 유형을 정의한 것으로 그들의 관계는 relationship 라고 부릅니다. model은 code로 생성할 수 있으나 Xcode는 실용적인 편집기를 제공하여 graph의 구조를 정의합니다.
model은 파일에 저장되고, 그 파일은 컴파일되어 우리 애플리케이션을 위해 생성된 Core Data 시스템에 포함됩니다. model의 이름은 상관없지만 확장자는 xcdatamodeld로 지정되어야 합니다.
File > New > File > iOS 하단에 내리면 Core Data를 추가할 수 있는 기능이 있습니다.
다음으로 model 파일의 이름과 저장할 곳을 지정하고 Create를 눌러줍니다.
그러면 다음과 같이 .xcdatamodeld 파일이 생성된 것을 확인할 수 있습니다.
Core Data Model
Model 파일은 3가지 크게 Entities, Attributes, Relationships 로 구성되어 있습니다. Entities는 객체, Attributes는 프로퍼티, Relationshps는 객체들(entities) 간의 연결입니다.
Entities
Entity는 좌측 하단에 Add Entity 버튼을 통해서 만들 수 있습니다. 버튼을 눌르면 기본 이름인 Entity로 Entity가 생성됩니다.
중요한 점은 entity의 이름은 반드시 대문자로 시작해야합니다. 이후 entity는 Object Graph(관계를 맺는 모든 객체) 의 일부가 될 object를 정의합니다.
Attributes
다음은 프로퍼티인 Attributes를 추가합니다. Attributes의 이름은 반드시 소문자로 지정하고 데이터 타입도 반드시 지정해야합니다.
Saving Image using Attributes
Attributes에서 이미지와 같은 큰 데이터는 시스템의 성능에 영향을 끼칠 수 있습니다. 이를 해결하기 위한 방법으로 이미지를 여러 파일에 나눠서 저장하는 방법이 있습니다. 방법은 어려울 것 같지만 Core Data는 이런 기능을 제공합니다. 우리가 해야할 것은 이미지의 데이터 타입을 Binary Data로 설정 후 Allows External Storage 를 선택하면 됩니다.
Making Connection with Entities
만일 한 사람에 의해서 책이 계속 출판된다고 가정했을 때 매번 저자의 이름을 따로 지정해서 만들수는 없기 때문에 Core Data에는 Relationships를 제공하여 Entity들 간의 관계를 설정할 수 있게 합니다. Relationship는 한 객체 내의 프로퍼티로 다른 객체의reference를 포함한 것입니다. Relationship은 하나 또는 여러개의 객체를 참조할 수 있습니다.
Relationships
Authors의 entity에서도 Books를 참조하는 Relationships로 설정하고 반대로 Authors에 의해 참조당하는 Books Entity에서도 Authors를 Relationships에 지정해야합니다.
Relationships를 설정하기 위해서는 Relationshp이 옵셔널일지(Properties), To-One 일지 아니면 To-Many일지(Type), 그리고 만일 Desination 객체가 지워졌을때 어떻게 대응할 것인지(Delete Rule)를 설정할 수 있습니다.
Delete Rule
Delete Rule에서 제대로된 Relationship의 delete rule를 찾기 위해서는 어떤 정보를 다루는지에 따라서 달라집니다. 이 예제에서는 간단합니다. 저자는 1개 이상의 책을 갖고 있기 때문에 만일 책이 지워졌다고 작가가 다른 책들도 썼을수도 있기 때문에 작가는 지워지면 안됩니다. Delete Rule에는 다음과 같은 4가지 케이스가 있습니다.
1. No Action: Destination 와 관계를 맺고 있는 데이터가 삭제되어도 Destination 에는 아무 행동도 하지 않는 것 입니다.
삭제된 객체에 대한 참조가 여전히 유효한 경우, 연결된 객체에서는 삭제된 객체를 참조하게 됩니다.
2. Nullify: 삭제되는 객체와 관련된 다른 객체들의 참조를 모두 null로 설정합니다.
삭제된 객체에 대한 참조를 제거하며, 연결된 객체들은 더 이상 삭제된 객체를 참조하지 않습니다.
3. Cascade: 삭제되는 객체와 관련된 다른 객체들을 재귀적으로 삭제합니다.
삭제된 객체와 직접 또는 간접적으로 연결된 모든 객체들이 삭제됩니다.
4. Deny Deleter: 삭제되는 객체와 관련된 다른 객체가 존재하는 경우, 삭제를 거부합니다.
다른 객체가 존재하는 한 삭제 작업이 실패하게 됩니다.
Fetched Properties
Core Data는 양방향 뿐만 아니라 단방향 연결인 Fetched Properties 도 지원합니다.
Inverse
Core Data는 Object graph의 일관성을 보장하기 위해서 Inverse를 필요로 하는데 Destination의 반대를 설정해주면 됩니다.
Core Data Stack
Core Data는 객체 그룹에서 생성되며, 이 그룹의 객체들은 객체 Object Graph부터 그래프의 저장까지 필요한 모든 프로세스를 관리합니다. Object Graph의 조직으로부터 해당 Object Graph를 데이터베이스에 저장하는 과정까지 모두 포함합니다. 모델을 관리하는 객체, 데이터를 파일에 저장하는 객체, 그리고 영속 저장소와 코드 간을 중개하는 객체가 있습니다, 이런 구성을 스택(stack)이라고 부릅니다.
다음은 위에서 언급한 스택(stack)이 구성되어 있는 모습을 나타낸 구조입니다.
Context는 Persistentcontainer에게 새로운 객체를 읽거나 객체를 추가하도록 요청합니다. Persistent container는 model에 따라 Object Graph를 처리하고 이를 파일에 저장합니다. Core Data 프레임워크는 Stack의 각 부분을 나타내는 객체를 생성하는 데 사용되는 클래스들을 제공합니다. NSManagedObjectModel 클래스는 model을 관리하며 NSPersistentStore는 모든 사용 가능한 PersistentStore를 관리하며, NSManagedObjectContext는 앱와 저장소 사이의 중간자인 context 만들고 관리합니다.
NSPersistentContainer
Core Data Stack의 객체들을 인스턴스화하고 스택을 직접 만들 수 있지만 프레임워크는 NSPersistentContainer 클래스를 만들어 모든 것을 관리할 수 있도록 해줍니다. NSPersistentContainer 클래스는 Core Data Stack을 캡슐화하는 컨테이너로 Core Data Stack 생성 및 관리를 단순화하기 위해, NSManagedObjectModel, NSManagedObjectContext, NSPersistentStoreCoordinator의 인스턴스 생성을 다룹니다.
NSPersistentContainer initializer
다음와 initializer과 프로퍼티들을 포함하여 각각의 객체와 stack에 접근할 수 있도록 해줍니다.
NSPersistentContainer(name: String): Core Data stack을 정의하는 NSPersistentContainer 객체를 만듭니다. name 인자는 container의 이름을 의미하며, name은 반드시 Core Data model의 파일 이름과 같아야합니다.
managedObjectModel: 이 프로퍼티는 Core Data model을 대표하는 NSManagedObjectModel 객체를 만들거나 반환합니다.
viewContext: 이 프로퍼티는 Object Graph를 접근하고 수정할 수 있는 stack의 context를 담당하는 NSManagedObjectContext 객체를 만들거나 반환합니다.
persistentStoreCoordinator: 이 프로퍼티는 가능한 모든 Persistent Stores를 관리하는 NSPersistentStoreCoordinator 객체를 만들거나 반환합니다.
NSPersistentContainer에서 conflict를 해결
NSPersistentContainer 객체는 문제가 발생 시 자동으로 해결해주는데 문제를 어떻게 해결할 것인지 context를 먼저 설정해야합니다. NSManagedObjectContext 클래스는 발생한 문제를 해결하기 위해 다음과 같은 프로퍼티를 제공합니다.
automaticallyMergesChangesFromParent: 이 프로퍼티는 Boolean 값으로 설정 또는 반환되며 context가 자동적으로 수정된 Persistent Store에 있는 내용과 context를 합칩니다.
mergePolicy: 이 프로퍼티는 context가 언제 수정된 Persistent Store에 있는 내용과 context를 합칠것인지에 규칙을 정합니다.
위에 프로퍼티를 설정하게 되었으면 문제가 발생하였을 경우 NSPersistentContainer 객체에서 문제를 해결해줍니다.
(필수) NSPersistentContainer에서 PersistentStore 관리하기
Core Data Stack을 만들기 위해서는 NSPersistentContainer 객체를 만들어야합니다. Persistent Store는 로드하는데 시간이 걸릴 수 있기 떄문에 반드시 객체를 만들어 Persistent Store를 비동기로 로드해야합니다. 그리하여 클래스는 다음과 같은 메서드를 제공합니다.
loadPersistentStore(completionHandler: Closure): 이 메서드는 Persistent Store를 로드하고 로드가 완료되었으면 Closure를 실행합니다. Closure는 2개의 인자를 받습니다. 1개는 NSPersistentStoreDesription 객체로 stack의 configuration을 나타내며 다른 하나는 optional Error로 에러를 나타냅니다.
예제 코드
NSPersistentContainer 객체를 만들어서 persistent store를 참조할 수 있게 합니다. 이후 객체가 초기화되면 Core Data 와 이름이 같은 NSPersistentContainer 클래스 오브젝트를 생성합니다. 이후 객체는 스택을 만들지만 persistent store를 로드하지는 않습니다. persistent store를 로드하는 것은 loadPersistentStores 메서드를 통해서 해야합니다. 이후 completion에서 error를 받게 된다면 fatalError를 통해서 앱을 중지시키고 에러 내용을 출력합니다.
class ApplicationData: ObservableObject {
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "books")
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { storeDescription, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
위에서 NSPersistentContainer를 만든 이후 반드시 context를 가져와서 뷰들과 함께 사용하여 객체들을 persistent store에 저장하거나 지울 수 있도록 합니다. 그리고 environment에는 NSManagedObjectContext 타입인 managedObjectContext 프로퍼티를 제공합니다. 이렇게 environment를 사용하여 다른 뷰들도 Core Data Context에 접근하여 persistent store로부터 객체를 fetch, 추가, 수정을 할 수 있게 합니다. environmentObject는 ApplicationData() 클래스 객체를 다른 뷰에서도 사용할 수 있게 해줍니다.
struct CoreDataApp: App {
@StateObject var appData = ApplicationData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appData)
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
}
NSManagedObjectModel
NSManagedObjectModel: 이 인스턴스는 Core Data Stack에 접근할 데이터를 나타냅니다. Core Data Stack 생성 과정의 첫번 째 스텝으로 NSMangedObjectModel이 메모리에 로드됩니다. NSManagedObjectModel이 초기화된 후에 NSPersistentStoreCoordinator 객체가 만들어집니다. 간단하게 Entity를 설명하는 Database의 스키마라고 생각하면 됩니다. 또 managed objects의 structure를 정의합니다.
NSManagedObjectContext
앱과 Persistent Store에 있는 데이터간의 모든 통신은 context를 통해서 진행됩니다. context는 NSManagedObjectContext 클래스로부터 만들어집니다. 이 클래스는 다음과 같은 프로퍼티들과 메서드들을 제공하여 context와 Persistent Store에 있는 객체들을 관리할 수 있게 합니다.
hasChanges: 이 프로퍼티는 Boolean값을 반환하는데 context가 Persistent Store에 값을 변경한 것이 있는지 유무를 반환합니다.
save(): 이 메서드는 변경된 사항을 Persistent Store에 저장합니다.
reset(): 이 메서드는 context의 상태를 초기화 합니다. 모든 객체들과 context에 담겨 있는 변경 사항들을 무시하고 초기화합니다.
fetch(NSFetchRequest): 이 메서드는 NSFetchRequest 객체로 요청받은 객체들을 배열로 반환합니다.
delete(NSManagedObject): 이 메서드는 Persistent Store에 있는 객체를 삭제합니다.
count(for: NSFetchRequest): 이 메서드는 Persistent Store에 요청한 데이터가 몇개나 있는지 반환하며 for 인자는 어떤 값의 개수를 찾을지 요구하는 부분입니다.
NSPersistentStoreCoordinator
NSPersistentStoreCoordinator는 Core Data Stack에서 중간 층에 위치하여 NSManagedObjectModel 인스턴스를 persistent storage(영구 저장소)에 저장하거나 fetch해옵니다. Coordinator는 모델 내부에 정의된 entity 인스턴스들을 실체화하는 역할을 합니다. 모델 내에서 새로운 entity 인스턴스들을 생성하거나 이미 존재하는 인스턴스를 persistent store로부터 탐색합니다. persistent store는 디스크 혹은 메모리에 존재할 수 있습니다. App의 구조에 따라서는 (일반적이진 않지만) NSPersistentStoreCoordinator에 의해 조직된 persistent store가 여러 개 일수도 있습니다
Model/Context/Coordinator 간 관계
NSManagedObjectModel가 데이터 구조를 정의한다면, NSPersistentStoreCoordinator는 persistent store에 있는 이 데이터를 객체로 실체화하여 요청하는 NSManagedObjectContext에게 전달합니다. 또한, NSPersistentStoreCoordinator는 데이터가 NSManagedObjectModel에서 정의한 것과 일치하는 일관된 상태인지를 검증합니다
NSManagedObjects
Core Data는 사용자 지정 객체를 직접 저장하지 않습니다. 대신에 이를 위한 NSManagedObject 클래스를 정의합니다. Persistent storage 에 데이터를 저장하기 위해서는 Managed Object를 만들고 Entity와 연결하여 해당 Entity가 허용하는 데이터를 저장해야 합니다. context에서 처리하는 실제 데이터는 Managed Object로 Managed 이름이 붙은 이유는 Core Data가 라이프 사이클을 관리하기 때문입니다.
Managed Object 란?
모델에 BookEntity라는 이름의 Entity가 있다고 가정했을때 아래처럼 subClassing을 통해서 NSManagedObject인 Entity Class를 생성합니다. 즉 BookEntity는 Managed Object가 되어 BookEntity 클래스를 통해 Entity와 연결하여 값을 저장할 수 있게 합니다.
subClassing: 서브 클래싱이란 다른 클래스로부터 상속받은 상속 클래스를 서브클래스(subclass)라고 합니다. 즉 subclassing은 subclassing은 클래스를 subclass로 만드는 것입니다.
class BookEntity: NSManagedObject {
@NSManaged var title: String
@NSManaged var author: String
}
Codegen
Codegen: 코드 생성을 의미하며 데이터 모델에서 생성된 NSManagedObject 클래스들을 관리하는 방식을 나타냅니다.
Class Definition (default): 이 옵션은 각 Entity에 대해 NSManagedObject의 subClass를 자동으로 생성합니다.
쉽게 말해 이 설정은 모델의 Entities, Attribute 들을 자동으로 Class화 시켜주고 Property화 시켜주는 역할을 한다. 추가로 모델의 변경되는 내용들도 자동으로 업데이트해줍니다. 엄청 좋은 것 같지만 추가적인 로직을 추가할 수 없다는 단점과 눈에 보이지 않기 때문에 subClass를 삭제하더라도 내부적으로 존재하여 파일이 꼬일 수 있다는 단점들도 있습니다.
Category/Extension: 이 옵션은 각 Entity에 대해 일반적인 NSManagedObject 클래스를 생성하고, Entity에 대한 확장(extension)을 생성합니다. 이.확장에서 Entity의 프로퍼티를 추가로 정의할 수 있습니다.
Manual/None: 이 옵션은 자동 생성을 사용하지 않고, 개발자가 직접 NSManagedObject 클래스를 작성하도록 합니다. 이 경우, Entity에서 변경되는 내용에 대응하는 클래스를 직접 갱신해야 합니다.
Codegen을 사용할때 발생하는 에러
에러 1
Multiple commands produce '/Users/taewonyoon/Library/Developer/Xcode/DerivedData/test-erwyataiqdyktwbikhdbkoqiyxyx/Build/Intermediates.noindex/test.build/Debug-iphoneos/test.build/Objects-normal/arm64/BookEntity+CoreDataClass.o'
위에 에러는 Codegen을 Class Definition으로 선택한 후 수동으로 NSManagedObject의 서브 클래스인 BookEntity를 만들게 된다면 에러가 발생합니다. 발생한 이유는 Class Definition 옵션을 선택하면 NSManagedObject의 서브 클래스가 내부에 자동으로 생성하기 때문에 서브 클래스가 중복되어 발생하는 오류입니다. Editor > Create NSManagedObject Subclass... 를 통해서 서브클래스를 수동으로 직접 생성해야하는 상황은 개발자가 Manual/None 옵션을 선택하였을 때 입니다.
에러 2
'Managed Object' is ambiguous for type lookup in this context
만일 Class Definition이 옵션으로 선택되었을 경우 Manual/None을 사용하려면 추가된 파일들을 삭제한 후 Manual/None 으로 선택하여 다시 파일들을 생성하는 방법과 Class Definition을 사용하려면 추가된 파일들을 삭제한 후 다시 빌드하는 방법이 있습니다. 이 방법으로 시도하였을때도 안된다면 Xcode를 재부팅하는 방법이 있습니다.
Managed Object 만드는법
위에 BookEntity는 다음과 같이 설정된 Entity로 Codegen이 Class Definition으로 되어 있다. Class Definition은 default 값으로 이름이 BookEntity인 Entity에 대해 NSManagedObject의 서브 클래스를 자동으로 생성하여 코드에서 직접 Entity의 속성에 접근할 수 있게 합니다.
이후 다음과 같이 ManagedObject 즉 NSManagedObject 타입인 BookEntity의 클래스 인스턴스 BookEntity를 생성하고 해당 인스턴스 프로퍼티에 값을 할당하는 예시입니다.
func saveBookToDatabase(title: String, author: String) {
// NSManagedObjectContext 가져오기
let context = persistentContainer.viewContext
// NSManagedObject 인스턴스 생성 및 엔터티와 연결
let newBook = BookEntity(context: context)
newBook.title = title
newBook.author = author
// 변경사항 저장
do {
try context.save()
print("Book saved successfully.")
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
Core Data는 모든 데이터를 Entity 단위로 처리합니다. context에서 처리하는 실제 데이터는 Managed Object로 Managed 이름이 붙는 이유는 Core Data가 라이프 사이클을 관리하기 때문입니다. Managed Object는 NSManagedObject 클래스로 구현되어 있습니다.
Managed Object는 NSManagedObject 클래스로 구현 되어 있으며 우리가 모델 파일에서 볼 수 있고 생성할 수 있는 Entity는 swift에서 NSManagedObject와 1대1로 연결됩니다. 다음은 NSManagedObject와 1대 1로 연결된 Entity를 인스턴스로 사용하는 예시입니다.
// NSManagedObject 인스턴스 생성 및 엔터티와 연결
let newBook = BookEntity(context: context)
데이터를 데이터베이스에 저장할때마다 반드시 Entity와 연관된 NSManagedObject 객체를 만들어야합니다. 그리고 Entity에 대한 객체와 Entity가 허용하는 데이터를 저장합니다. 예시로 책을 persistent store에 저장하기로 했을 경우 NSManagedObject 클래스 초기화를 사용해 책의 relationship과 attributes의 값들 인스턴스를 만들어야합니다.
NSManagedObject 클래스는 다음과 같은 메서드들을 제공하여 객체들을 만들고 관리할 수 있게 합니다.
NSManagedObject(context: NSManagedObjectContext): 이 메서드는 NSManagedObject 클래스 또는 서브클래스의 새로운 인스턴스를 만들고 context 인자값으로 context를 넣을 수 있습니다.
// Core Data에서 사용할 엔터티에 대응하는 NSManagedObject 클래스 정의
class MyEntity: NSManagedObject {
@NSManaged var attributeName: String
}
// NSManagedObjectContext 생성
let context = persistentContainer.viewContext
// NSManagedObject 초기화 및 속성 설정
let newManagedObject = MyEntity(context: context)
newManagedObject.attributeName = "Example Data"
fetchRequest(): 이 타입 메서드는 entity를 위한 fetch request를 생성합니다. fetch reqeust는 특정 Persistent Store에 있는 특정 entity의 객체를 Persistent Store로부터 가져오도록 한 다음 context로 옮길 수 있습니다. 객체를 context로 옮긴 이후 객체의 프로퍼티를 읽고 수정하고 값을 삭제 또는 해당 객체의 데이터 개수같은 정보를 사용할 수 있습니다.
entity(): 이 타입 객체는 어떤 managed object가 만들어졌는지에 다른 entity의 참조를 반환합니다. 이 객체의 타입은 NSEntityDescription으로 entity의 설명을 나타냅니다.
데이터 가져오기
NSFetchRequest
Core Data는 NSFetchRequest 클래스를 제공하여 객체를 Persistent Store로부터 객체를 가져올 수 있습니다. Persistent Store로부터 데이터를 가져오기 위해서는 반드시 NSFetchRequest를 사용하여 원하는 객체를 결정해야합니다.
NSFetchRequest 클래스에서 제공하는 메서드
predicate: 이 프로퍼티를 사용하면 조건을 설정하여 원하는 값을 가져올 수 있습니다.
sortDescriptors: 이 프로퍼티를 사용하면 결과를 정렬할 수 있습니다.
fetchLimit: 이 프로퍼티를 사용하면 반환받는 객체의 개수를 제한할 수 있습니다.
propertiesToFetch: 이 프로퍼티를 사용하면 검색된 객체에서 가져올 프로퍼티를 지정하는데 사용됩니다.
@FetchRequest
SwiftUI는 @FetchRequest 프로퍼티 래퍼를 제공하는데 이 프로퍼티는 request와 반환된 데이터를 뷰에 적용하는 역할을 합니다.
FetchRequest 선언방법
FetchRequest(sortDescriptors: [SortDescripor], predicate: NSPredicate?, animation: Animation?): 이 선언방법은 FetchRequest를 만드는데 사용되며 Persistent Store에 데이터를 검색할 때 사용되는 객체로, 검색 조건, 정렬 조건, 애니메이션을 설정하는데 사용됩니다. sortDescriptors는 검색 결과를 정렬을 지정하는 배열을 인자로 받으며 predicate는 검색 조건을 지정하는데 사용됩니다. animation은 검색 결과로 인해 값이 변경된 때 UI에 애니메이션을 적용합니다.
struct ContentView: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \MyEntity.timestamp, ascending: true)],
predicate: NSPredicate(format: "name CONTAINS %@", "SearchKeyword"),
animation: .default
)
private var entities: FetchedResults<MyEntity>
var body: some View {
List {
ForEach(entities) { entity in
Text(entity.name ?? "Unknown")
}
}
}
}
FetchRequest(fetchRequest: NSFetchRequest, animation: Animation?): 이 선언방법은 NSFetchRequest 객체를 인자로 받는데 NSFetchRequest 객체는 predicate, sortDescriptors 등이 설정되어 있습니다. animation은 겸색 결과로 인해 값이 변경될 때 UI에 애니메이션을 적용합니다.
struct ContentView: View {
@FetchRequest(fetchRequest: MyEntity.fetchRequest(), animation: .default)
private var entities: FetchedResults<MyEntity>
var body: some View {
List(entities) { entity in
Text(entity.attributeName)
}
}
}
@FetchRequest로 데이터 가져오기
가장 먼저 저장된 객체들을 가져오기 위해서는 View는 @FetchRequest 프로퍼티를 만들어야합니다. 이 프로퍼티는 FetchRequest를 만들어 books Entity 객체를 정렬하지 않고 가져오게끔 합니다. 그리고 생성된 listOfBooks 프로퍼티 래퍼는 FetchedResults<Books> 제너릭 타입으로 선언되어 있습니다. FetchedResults에 들어갈 타입은 가져올 객체의 데이터 타입을 의미하며 아래에서는 Books 객체를 의미합니다. @FetchRequest 프로퍼티를 설정하면 persistent store로부터 데이터를 가져옵니다
struct ContentView: View {
@FetchRequest(sortDescriptors: [], predicate: nil, animation: .default) private var listOfBooks: FetchedResults<Books>
var body: some View {
NavigationStack {
List {
ForEach(listOfBooks) { book in
RowBook(book: book)
}
}
.navigationBarTitle("Books")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: InsertBookView(), label: {
Image(systemName: "plus")
})
}
}
}
}
}
위에서 사용된 RowBook는 아래와 같습니다. 2번째 Text를 보면 book.author를 통해서 객체의 Relationships에 정의된 값에도 접근할 수 있습니다. 그리고 thumbnail는 Binary Data 타입으로 이미지를 저장하고 있으며 저장된 thumbnail은 UIImage를 통해서 Binary Data 타입 데이터를 변환하여 사용해야합니다.
struct RowBook: View {
let book: Books
var imageCover: UIImage {
if let data = book.thumbnail, let image = UIImage(data: data) {
return image
} else {
return UIImage(named: "nopicture")!
}
}
var body: some View {
HStack(alignment: .top) {
Image(uiImage: imageCover)
.resizable()
.scaledToFit()
.frame(width: 80, height: 100)
.cornerRadius(10)
VStack(alignment: .leading, spacing: 2) {
Text(book.title ?? "Undefined")
.bold()
Text(book.author?.name ?? "Undefined")
.foregroundColor(book.author != nil ? .black : .gray)
Text(String(book.year))
.font(.caption)
Spacer()
}.padding(.top, 5)
Spacer()
}.padding(.top, 5)
}
}
NSManagedObjectcontext를 이용한 데이터 저장하기
만일 이미 저장된 객체에 접근하거나 새로운 객체를 저장하거나 삭제 또는 수정을 하고싶을 경우 context를 통해서 진행해야합니다. 그리고 변경된 사항들을 context에서 persistent storage로 옮기는것 입니다. FetchRequest 프로퍼티 래퍼는 자동으로 environment를 통해서 참조된 context를 사용합니다.
context는 environment에서 \.managedObjectContext 키값으로 접근이 가능합니다.
@Environment(\.managedObjectContext) var dbContext
context는 save() 메서드를 사용하여 context에 담긴 데이터를 persistent store에 저장합니다.
try dbContext.save()
비동기적으로 데이터 저장하기, 가져오기
데이터를 persistent store를 통해서 가져오거나 저장하기 위해서는 같은 스레드를 통해 진행해야합니다. 만일 다른 스레드가 동시에 하나의 값을 가져오거나 저장할 경우 에러가 발생할 수 있기 때문입니다. 이런 문제를 피하기 위해서는 NSManagedObjectContext는 다음과 같은 비동기적 메서드를 제공합니다.
perform(schedule: ScheduledTaskType, Closure): 이 비동기적 메서드는 Core Data context에 할당된 스레드로 closure 내부 작업을 수행합니다. ScheduledTaskType 인자는 어떻게 closure가 시작될 것인지를 결정하고 ScheduledTaskType은 예약하는 enqueued와 immediate 값이 있습니다.
struct InsertBookView: View {
@Environment(\.managedObjectContext) var dbContext
@Environment(\.dismiss) var dismiss
@State private var inputTitle: String = ""
@State private var inputYear: String = ""
var body: some View {
VStack(spacing: 12) {
HStack {
Text("Title:")
TextField("Insert Title", text: $inputTitle)
.textFieldStyle(.roundedBorder)
}
HStack {
Text("Year:")
TextField("Insert Year", text: $inputYear)
.textFieldStyle(.roundedBorder)
}
HStack {
Text("Author:")
Text("Undefined")
.foregroundColor(.gray)
}.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
Spacer()
}.padding()
.navigationBarTitle("Add Book")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
let newTitle = inputTitle.trimmingCharacters(in: .whitespaces)
let newYear = Int32(inputYear)
if !newTitle.isEmpty && newYear != nil {
Task(priority: .high) {
await storeBook(title: newTitle, year: newYear!)
}
}
}
}
}
}
func storeBook(title: String, year: Int32) async {
await dbContext.perform {
let newBook = Books(context: dbContext)
newBook.title = title
newBook.year = year
newBook.author = nil
newBook.cover = UIImage(named: "bookcover")?.pngData()
newBook.thumbnail = UIImage(named: "bookthumbnail")?.pngData()
do {
try dbContext.save()
dismiss()
} catch {
print("Error saving record")
}
}
}
}
이후 추가적으로 다음과 같이 extension을 사용하여 좀 더 간결하게 코드를 만들 수 있습니다.
extension Books {
var showTitle: String {
return title ?? "Undefined"
}
var showYear: String {
return String(year)
}
var showAuthor: String {
return author?.name ?? "Undefined"
}
var showCover: UIImage {
if let data = cover, let image = UIImage(data: data) {
return image
} else {
return UIImage(named: "nopicture")!
}
}
var showThumbnail: UIImage {
if let data = thumbnail, let image = UIImage(data: data) {
return image
} else {
return UIImage(named: "nopicture")!
}
}
}
extension Authors {
var showName: String {
return name ?? "Undefined"
}
}
Previews 으로 메모리에 데이터 저장
Preview를 통해서 테스트하려면 2개의 Persistent Store를 정의해야합니다. 다음 예제에서는 type property를 사용하여 구현하였습니다. init(preview: Bool) 메서드를 사용하여 preview가 true면 container의 persistentStoreDescriptions에서 첫 번째 store의 URL을 /dev/null로 설정하는데 이렇게 하면 데이터가 실제로 디스크 저장되지 않고 메모리에서만 작업하게 됩니다. false인 경우 디스크에 만들어집니다.
class ApplicationData: ObservableObject {
let container: NSPersistentContainer
static var preview: ApplicationData = {
let model = ApplicationData(preview: true)
return model
}()
init(preview: Bool = false) {
container = NSPersistentContainer(name: "books")
if preview {
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)")
}
})
}
}
위처럼 만들었으면 environment 를 사용하여 ApplicationData 클래스에서 정의한 ApplicationData 클래스인 preview 클래스의 container의 viewContext를 설정하여 해당 뷰에서도 preview의 managedObjectContext를 사용할 수 있게 하였습니다.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.managedObjectContext, ApplicationData.preview.container.viewContext)
}
}
Sort Descriptors
SortDescriptor는 FetchRequest 프로퍼티 래퍼와 함께 쓰이도록 만들어졌습니다. SwiftUI 애플리케이션에서 SortDescriptor가 가장 많이 사용되는 구조체입니다. 이것을 사용하여 특정 프로퍼티의 값을 정렬할 수 있습니다. SortDescriptor는 다음과 같은 메서드를 제공하고 있습니다.
SortDescriptor(KeyPath, comparator: StandardCompartor, order: SortOrder): 이 선언자는 SortDescriptor 구조체를 만들어 FetchRequest 첫번째 인자에 명시된 객체의 프로퍼티의 값을 정렬합니다. 그리하여 객체를 정렬하기 위해서는 최소 하나의 SortDescriptor를 만들어 FetchRequest 인자에 넣어야합니다.
다음은 Books 객체를 title로 오름차순으로 정렬하는 코드입니다. 그리고 두 번째 정렬 기준으로 Books의 year 프로퍼티를 기준으로 오름차순 정렬을 하는 코드입니다.
@FetchRequest(sortDescriptors: [SortDescriptor(\Books.author?.name, order: .forward), SortDescriptor(\.year, order: .forward)], predicate: nil, animation: .default) var listOfBooks: FetchedResults<Books>
SortDescriptor는 view를 통해서 변경될 수 있는데 만약 값이 변경되면 해당 값이 @FetchRequest 프로퍼티 래퍼에 적용되어 변경되는 순간 해당 변경사항에 대응하여 뷰를 업데이트합니다.
struct ContentView: View {
@FetchRequest(sortDescriptors: [SortDescriptor(\Books.title, order: .forward)], predicate: nil, animation: .default) var listOfBooks: FetchedResults<Books>
var body: some View {
NavigationStack {
List {
ForEach(listOfBooks) { book in
RowBook(book: book)
}
}
.navigationBarTitle("Books")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Menu("Sort") {
Button("Sort by Title", action: {
let sort = SortDescriptor(\Books.title, order: .forward)
listOfBooks.sortDescriptors = [sort]
})
Button("Sort by Author", action: {
let sort = SortDescriptor(\Books.author?.name, order: .forward)
listOfBooks.sortDescriptors = [sort]
})
Button("Sort by Year", action: {
let sort = SortDescriptor(\Books.year, order: .forward)
listOfBooks.sortDescriptors = [sort]
})
}
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: InsertBookView(), label: {
Image(systemName: "plus")
})
}
}
}
}
}
Predicates 검색 조건을 설정하기
Predicates는 특정 Entity의 데이터를 가져오기 위한 조건을 설정하는 것입니다. 이를 위해 Foundation 프레임워크는 NSPredicate 클래스를 제공하고 있습니다.
NSPredicate(format: String, argumentArray: [Any]?): 이 초기화를 사용하여 format 인자에 조건을 설정한 NSPredicate 객체를 만들어 @FetchRequest 프로퍼티 래퍼에 제공할 수 있습니다.
다음은 1983년에 출판된 책을 가져오는 @FetchRequest 프로퍼티 래퍼입니다.
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "year = 1983")) var listOfBooks: FetchedResults<Books>
더 나아가 Relationships로 등록된 Entity에도 '.'을 사용하여 접근할 수 있습니다. 다음은 author에서 name이 Stenphen King을 찾는 것입니다.
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "author.name = 'Stephen King'")) var listOfBooks: FetchedResults<Books>
@FetchRequest 프로퍼티 래퍼 initializer에서 뿐만 아니라 선언된 이후에 조건을 만들거나 수정하여 persistent storage에서 수정된 값을 가져올 수 있는데 이것을 제공하는 메서드는 nsPredicate입니다. 다음과 같이 재설정할 수 있습니다.
listOfBooks.nsPredicate = NSPredicate(format: "year = %@", NSNumber(value: year))
다음은 author.name의 값이 value로 시작하는 값을 찾습니다. 하지만 소문자 c로 설정하였기에 대문자 소문자 구분없이 찾습니다.
listOfBooks.nsPredicate = NSPredicate(format: "author.name BEGINSWITH[c] %@", value)
다음도 많이 사용되는 predicate인데 CONATINS는 무언가를 포함하고 있는 의미입니다. dc는 term의 ery처럼 또는 misery의 ery처럼 value 값으로 ery를 넣으면 ery를 포함된 값을 가져옵니다.
listOfBooks.nsPredicate = NSPredicate(format: "title CONTAINS[dc] %@", value)
다음처럼 ANY, ALL, NONE을 사용하여 원하는 값을 가져올 수 있습니다.
ANY: 일부만 같아도 데이터를 가져옵니다.
ALL: 조건이 완전히 같아야지만 데이터를 가져옵니다.
NONE: 모든 조건이 틀렸을 경우 데이터를 가져옵니다.
request.predicate = NSPredicate(format: "ANY author.name == %@", "Stephen King")
중요한점은 뷰 간 이동을 하여 다른 뷰에서 view를 실행하였을때 @FetchRequest 프로퍼티 래퍼가 값을 가져오지 않았는데 뷰가 @FetchRequest 프로퍼티 래퍼를 통해 가져온 데이터를 사용하려고 하면 에러가 나기 때문에 반드시 "FALSEPREDICATE"로 설정해야합니다. 이것은 처음에는 아무값도 가져오지 않는다는 것을 의미합니다. 그런 다음 init을 통해 새로운 FetchRequest 구조체를 만들어 @FetchRequest에 할당합니다.
struct AuthorBooksView: View {
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "FALSEPREDICATE")) var listOfBooks: FetchedResults<Books>
var selectedAuthor: Authors?
init(selectedAuthor: Authors?) {
self.selectedAuthor = selectedAuthor
if selectedAuthor != nil {
_listOfBooks = FetchRequest(sortDescriptors: [SortDescriptor(\Books.title, order: .forward)], predicate: NSPredicate(format: "author = %@", selectedAuthor!), animation: .default)
}
}
var body: some View {
List {
ForEach(listOfBooks) { book in
Text(book.title ?? "Undefined")
}
}
.navigationBarTitle(selectedAuthor?.name ?? "Undefined")
}
}
추가로 조건은 AND, NOT, OR 로직을 사용하는 방법도 있습니다.
조건문에 대한 기호들은 매우 다양한데 다음 링크에 잘 정리되어 있습니다.
Modifying Objects
persistent store의 객체를 수정하는 방법은 쉽습니다. 해야할 일은 view에 객체를 전달하여 유저가 객체의 값을 수정할 수 있게 합니다.
먼저 persistent storage에서부터 객체를 가져옵니다, 가져온 데이터를 RowBook에 전달하는데 뒤에 .id(UUID())와 함께 전달하여 어떤 데이터를 전달하는지 구분할 수 있게 합니다.
struct ContentView: View {
@FetchRequest(sortDescriptors: [], predicate: nil, animation: .default) var listOfBooks: FetchedResults<Books>
@State private var search: String = ""
var body: some View {
NavigationStack {
List {
ForEach(listOfBooks) { book in
NavigationLink(destination: ModifyBookView(book: book), label: {
RowBook(book: book)
.id(UUID())
})
}
}
.navigationBarTitle("Books")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: InsertBookView(), label: {
Image(systemName: "plus")
})
}
}
.searchable(text: $search, prompt: Text("Insert Title"))
.onChange(of: search) { value in
if !value.isEmpty {
listOfBooks.nsPredicate = NSPredicate(format: "title CONTAINS[dc] %@", value)
} else {
listOfBooks.nsPredicate = nil
}
}
}
}
}
이후 가져온 데이터를 다음 함수로 수정하여 persistent storage에 변경사항을 저장합니다.
@Environment(\.managedObjectContext) var dbContext
let book: Books? // ManagedObject
func saveBook(title: String, year: Int32) async {
await dbContext.perform {
book?.title = title
book?.year = year
book?.author = selectedAuthor
do {
try dbContext.save()
dismiss()
} catch {
print("Error saving record")
}
}
}
Deleting Objects
다음은 만들어진 객체를 삭제하는 작업입니다. 아주 간단합니다. 가져온 데이터를 \.managedObjectContext에 담에서 delete 메서드를 사용하면 삭제할 수 있습니다.
struct ContentView: View {
@Environment(\.managedObjectContext) var dbContext
@FetchRequest(sortDescriptors: [], predicate: nil, animation: .default) var listOfBooks: FetchedResults<Books>
var body: some View {
NavigationStack {
List {
ForEach(listOfBooks) { book in
RowBook(book: book)
}
.onDelete(perform: { indexes in
Task(priority: .high) {
await deleteBook(indexes: indexes)
}
})
}
.navigationBarTitle("Books")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: InsertBookView(), label: {
Image(systemName: "plus")
})
}
}
}
}
func deleteBook(indexes: IndexSet) async {
await dbContext.perform {
for index in indexes {
dbContext.delete(listOfBooks[index])
}
do {
try dbContext.save()
} catch {
print("Error deleting objects")
}
}
}
}
Custom Fetch Requests
다음처럼 코드를 작성하면 특정 객체에 값이 몇개나 있는지 확인할 수 있습니다.
func countBooks() {
let request: NSFetchRequest<Books> = Books.fetchRequest()
if let count = try? self.dbContext.count(for: request) {
totalBooks = count
}
}
중복된 데이터 저장 피하기
중복되는 데이터 저장을 피하기 위해서는 기존 저장소에 중복되는 데이터가 있는지 확인한 predicate을 인자로 받는 request를 사용해야합니다.
다음 코드는 기존에 데이터가 있는지 확인하는 NSFetchRequest를 만들어 만일 predicate로 설정된 조건과 같은 데이터가 없을 경우 데이터를 저장하는 코드입니다.
func storeAuthor(name: String) async {
await dbContext.perform {
let request: NSFetchRequest<Authors> = Authors.fetchRequest()
request.predicate = NSPredicate(format: "name = %@", name)
if let total = try? self.dbContext.count(for: request), total == 0 {
let newAuthor = Authors(context: dbContext)
newAuthor.name = name
do {
try dbContext.save()
} catch {
print("Error saving record")
}
}
}
}
Sections
Section에서도 사용할 수 있게 만든 SectionFetchRequest 코드를 사용합니다. sectionIdentifier를 통해서 Books의 author.name으로 Section을 구분하도록 설정하였습니다.
struct ContentView: View {
@SectionedFetchRequest(sectionIdentifier: \Books.author?.name, sortDescriptors: [SortDescriptor(\Books.author?.name, order: .forward)], predicate: nil, animation: .default) private var sectionBooks: SectionedFetchResults<String?, Books>
var body: some View {
NavigationStack {
List {
ForEach(sectionBooks) { section in
Section(header: Text(section.id ?? "Undefined")) {
ForEach(section) { book in
NavigationLink(destination: ModifyBookView(book: book), label: {
RowBook(book: book)
.id(UUID())
})
}
}
}
}
.navigationBarTitle("Books")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: InsertBookView(), label: {
Image(systemName: "plus")
})
}
}
}
}
}
To-Many Relationships
우선 Relationships 의 Type을 To Many로 설정해야합니다. 관계가 복잡하지 않을때는 Core Data가 객체간의 연결을 다 처리했지만 관계가 점점 많아지면 직접 값을 읽고 써야합니다. To Many relationship의 값은 NSSet 객체에 저장되어 있습니다. NSSet 클래스는 foundation 프레임워크로부터 정의되어 set로된 값들을 저장합니다. NSSet에 있는 값을 읽기 위해서는 Swift set으로 캐스트하여 읽을 수 있습니다. 하지만 swift의 set 또는 array를 NSSet 객체로 바꾸기 위해서는 다음과 같은 initializer를 설정해야합니다.
NSSet(set: Set): 이 initializer는 attrubute가 제공하는 set로 NSSet 객체를 만듭니다.
NSSet(array: Array): 이 initialzier는 attrubute가 제공하는 array로 NSSet 객체를 만듭니다.
다음은 실제 Managed Object 클래스에 저장된 NSSet의 모습입니다.
extension Books {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Books> {
return NSFetchRequest<Books>(entityName: "Books")
}
@NSManaged public var cover: Data?
@NSManaged public var firstLetter: String?
@NSManaged public var thumbnail: Data?
@NSManaged public var title: String?
@NSManaged public var year: Int32
@NSManaged public var author: NSSet?
}
다음처럼 선언된 Relationship의 값을 읽기 위해서는 view model에서 코드를 작성할 수 있습니다.
다음 예제 코드에서는 가장 먼저 ',' 콤마로 나눠져있는 string타입의 list에서 author들을 받는 프로퍼티를 만듭니다. 방법은 다음과 같습니다. ShowAuthors 프로퍼티는 list 변수에 author 프로퍼티를 Set 타입으로 캐스팅합니다. 이 Set은 Books에 할당된 set의 Authors 객체로 map을 사용하여 listNames 배열에 발견한 $0.name의 값들을 넣습니다. 만약 listNames가 비어있지 않다면 배열에 값들을 하나의 String으로 만든 authors로 반환합니다.
var showAuthors: String {
var authors: String!
if let list = author as? Set<Authors> {
let listNames = list.map({ $0.name ?? "Undefined" })
if !listNames.isEmpty {
authors = listNames.joined(separator: ", ")
}
}
return authors ?? "Undefined"
}
만약 to-many relationship에 대하여 가져온 Set값에 대해 읽기-쓰기 권한을 얻기 위해서는 mutableSetValue(forKey:) 메서드를 사용해야합니다. 이후 forKey에는 수정하고 싶은 NSSet의 이름을 String으로 넣습니다. 그러면 NSSet의 이름으로 해당 NSSet을 NSMutableSet으로 반환받아서 수정을 할 수 있게 됩니다.
extension Books {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Books> {
return NSFetchRequest<Books>(entityName: "Books")
}
@NSManaged public var cover: Data?
@NSManaged public var firstLetter: String?
@NSManaged public var thumbnail: Data?
@NSManaged public var title: String?
@NSManaged public var year: Int32
@NSManaged public var author: NSSet?
}
let authorSet = book.mutableSetValue(forKey: "author")
authorSet.remove(author)
book.author = authorSet
'SwiftUI' 카테고리의 다른 글
kSecClass & Item Class Value in Keychain - 키체인 (0) | 2023.11.18 |
---|---|
Keychain services (0) | 2023.11.16 |
EnvironmentObject (0) | 2023.11.15 |
NSCoding (0) | 2023.11.13 |
Archiving, encoding, decoding (0) | 2023.11.13 |