감자주먹밥

[SwiftUI] CoreData + CloudKit으로 데이터 관리하기 본문

IOS/SwiftUI

[SwiftUI] CoreData + CloudKit으로 데이터 관리하기

JustHm 2023. 6. 13. 19:09
728x90

CoreData는 UserDefaults와 비슷하다 생각할 수 있지만, UserDefaults는 간단한 데이터 정도의 저장이 적합하고, CoreData는 UserData, 큰 데이터를 저장하기 용이하다.

여기서 CloudKit까지 같이 사용해 준다면, GoodNote 어플처럼 따로 서버를 두지 않고 여러 기기의 앱 내에서 데이터를 동기화시킬 수 있다.

key-value형식이나 파일, 또는 CoreData Model 형식과 비슷하게 저장할 수 있는데 GoodNote는 파일 형식으로 저장해서 사용하는 것 같다.

SwiftData가 곧 나와 금방 잊혀질 것 같으니 미리 공부해 적용해 봤다.


구현

먼저 프로젝트 생성시에 Use CoreData, Host in CloudKit을 체크하여 생성한다.

체크하면 Persistence 파일이 생성되고 CoreData + Cloudkit을 사용할 수 있는 기본적인 세팅과 예제와 함께 프로젝트가 생성된다.

만약 체크를 안했더라도 그냥 파일 추가와 capability를 추가하고 세팅을 해주면 된다.

BackgroundModes에 remote notification 체크, PushNotification 추가, iCloud 추가 

BackgroundModes의 remote notification는 cloud의 변경 사항이 있을 때 notification을 받아 데이터를 처리할 수 있다.

iCloud의 3가지 Service가 존재하는데 iCloudKit을 선택하고 Containers에 데이터를 저장할 iCloud의 Container이름을 추가 후 체크하면 된다.

데이터를 클라우드에도 연동해서 저장하려면 NSPersistentCloudKitContainer을 사용해야 한다.

먼저 CoreData + CloudKit을 사용하려면 Container를 초기화해줘야 한다.

private let container: NSPersistentCloudKitContainer

init(inMemory: Bool = false) {
    //생성했던 컨테이너 이름으로 초기화
    container = NSPersistentCloudKitContainer(name: "BeautyCarte")
    if inMemory {
        container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    }
    //컨테이너 load
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    container.viewContext.automaticallyMergesChangesFromParent = true
    fetchCustomer()
}

private func fetchCustomer(filter: String = "") {
    let request = NSFetchRequest<Customer>(entityName: "Customer")
    do {
        customers = try container.viewContext.fetch(request)
    } catch {
        print("ERROR FETCHING CORE DATA")
        print(error.localizedDescription)
    }
}

// container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // in-memory와 영구 저장소 merge 충돌: in-memory우선
// container.viewContext.shouldDeleteInaccessibleFaults = true // 접근 불가의 결함들을 삭제할 수 있게끔 설정
// container.viewContext.automaticallyMergesChangesFromParent = true // parent의 context가 바뀌면 자동으로 merge되는 설정

데이터를 Fetch 하는 것은 container를 통해 할 수 있다.

SwiftUI에서는 @FetchRequest Property wrapper를 사용해 실시간으로 받을 수 도 있다.

@FetchRequest(entity: Customer.entity(), sortDescriptors: [])
var customers: FetchedResults<Customer>

간단한 데이터가 하나 있을 땐 관리하기 편하다. 변수 그대로 사용하여 값을 추가하거나, 값을 변경, 삭제하면 자동으로 반영된다.

조금 복잡한 데이터를 관리하게 되면 이 방식 보다 위처럼 container를 초기화해서 CRUD를 해주면 된다.

CoreData를 사용하기 위한 세팅이 끝나면 이제 Data Model을 만들어 주면 된다.

왼쪽 아래의 Add Entity를 클릭해 추가할 수 있고, Attributes에 +를 눌러 필드를 추가할 수 있다.

relationShip의 경우 Entity끼리 연결하여 사용할 수 있다. 주의할 점은 연결할 두 Entity에 양방향으로 추가해줘야 한다. 그러면 Inverse에 자동으로 설정이 된다.

CRUD도 간단하다. 

@FetchRequest는 처음 말했던 것처럼 그대로 사용하면 되고, 만약 FetchRequest를 사용하지 않는다면 코드로도 간단히 구현할 수 있다.

// CoreData에 저장되어 있는 데이터 불러오기
private func fetchCustomer(filter: String = "") {
    let request = NSFetchRequest<Customer>(entityName: "Customer")
    if !filter.isEmpty {
        request.predicate = NSPredicate(format: "name CONTAINS[cd] %@", filter)
    }
    do {
        customers = try container.viewContext.fetch(request)
    } catch {
        print("ERROR FETCHING CORE DATA")
        print(error.localizedDescription)
    }
}

NSFetchRequest 객체를 생성한다. 그 후 필요하다면 Predicate도 추가해 데이터를 가져오면서 조건을 걸 수 있다.

다른 조건들도 많겠지만,, 여기서 사용한 조건문은 name 필드에 filterText를 포함하는 data를 가져온다.

NSFetchRequest가 정의되었다면, container.viewContext.fetch 메서드에 request를 넣어 호출하면 데이터를 가져올 수 있다.

private func saveData() {
    do {
        try container.viewContext.save()
        fetchCustomer()
    } catch {
        print("ERROR SAVING CORE DATA")
        print(error.localizedDescription)
    }
}

func deleteObject(object: NSManagedObject) {
    container.viewContext.delete(object)
    saveData()
}

저장과 삭제는 간단하다. 초기화해 둔 container의 viewContext에서 save(), delete(_:NSManagedObject) 메서드를 사용하면 된다.

그런데 save에서는 추가되는 Entity를 넣는 파라미터가 없다. 이건 그냥 viewContext로 Entity를 생성해서 값을 주입해 놓고 save 메서드를 사용하면 된다.

delete시 relationShip Entity가 있다면 하나씩 들어가 지워줘야 하는데, Model에서 추가할 때 Delete Rule을 Cascade로 설정해 주면 연관된 모든 데이터를 지울 수 있다.

아까 설명을 못하고 넘어갔는데, Type에 To One, To Many가 있어 RelationShip Entity를 단일 또는 다중으로 사용할 수 있다.

func addCustomer(name: String, phone: String, id: UUID = UUID()) {
    let customer = Customer(context: container.viewContext)
    customer.id = id
    customer.name = name
    customer.phone = phone
    saveData()
}

func updateConsulting(consulting: Consulting) {
    guard let temp = try? container.viewContext.existingObject(with: consulting.objectID) as? Consulting
    else { return }
    temp.text = consulting.text
    //Drawing 어케할지 생각좀
    saveData()
}

update의 경우 현재 선택한 Entity가 뭔지 알아야 하기 때문에 Entity의 objectID로 Entity를 받아오고 값을 변경 후에 다시 저장하면 된다.

처음에는 그냥 받아온 데이터를 그대로 업데이트하고 저장하면 될 줄 알았는데, Fetch 해서 가져온 데이터에 바로 할 때만 되고 따로 빼내서 사용할 땐 적용이 안되기 때문에 container에서 objectID로 찾아 업데이트하는 방식을 사용했다.

UIKit에도 당연히 똑같이 적용 가능

https://icloud.developer.apple.com

 

로그인 - Apple

 

idmsa.apple.com

추가로 이 사이트에 들어가서 icloud의 컨테이너를 선택해 저장되는 데이터를 확인 할 수 있다.


참고하기.

 

Setting Up Core Data with CloudKit | Apple Developer Documentation

Set up the classes and capabilities that sync your store to CloudKit.

developer.apple.com

 

Core Data | Apple Developer Documentation

Persist or cache data on a single device, or sync data to multiple devices with CloudKit.

developer.apple.com

 

Core Data (1)

안녕하세요 :) Zedd입니다. Core Data를 사용할 일이 생겼는데...제가 옜날에 해봤단 말이죠..!?!?!? 근데 다시 하려니까 생각이 하나도 안나는거에요 그때는 문서 볼 생각도 안했었는데..ㅎㅎㅎㅎ 이

zeddios.tistory.com

 

728x90
Comments