...
본문 바로가기

iOS

CoreData와 CloudKit 연동하기


 

 

1. 세팅하기 

우선 가장 좋은 방법은, 애초에 프로젝트를 만들 때 Use Core Data, Use CloudKit을 체크하는 게 좋다. 

그리고 target에서 Signing에서, "Automatically mannage signing을 선택하고,

개발팀을 선택한다.

그리고 iCloud를 가능하게 만들어야 한다.

  • target - Capabilities 에서 iCloud를 추가하고, CloudKit을 선택한다.
    • 이렇게 되면 자동적으로 Push notification이 추가된다. 이를 통해 remote content가 변경됐을 때 알려주기 때문이다.
  • Use default container를 선택한다. 
    • ( 하지만! Xcode11부터는 더 이상 Use default container 체크박스가 없어졌고, 새로 + 버튼을 눌러서 만들어야 한다. )

 

추가적으로 iCloud를 세팅하는 방법은 여기서 확인할 수 있다

  • 우선, Apple Developer Program memebership이 돼있어야 하고, admin permission을 가져야 한다.
  • 앱을 위해 사용하기 위한 bundle identifier를 확실히 한다. 이 identifier가 나중에 생성될 iCloud container의 이름을 결정한다. 
  • CloudKit을 체크하면, iCloud Container가 생성된다. 
    • 여러 앱과 유저들은 iCloud에 접근할 수 있지만, 각 앱의 data, 스킴들은 다른 container에 있다.  비록 앱은 여러 개의 container가질 수 있고, container를 공유할 수 있지만, 각 앱들은 하나의 default container를 갖는다.  일단 container를 만들고 나면 삭제하거나 이름을 변경할 수 없다.
  • 그리고 container에 record를 저장하기 위해서는 iCloud 계정이 필요하다.  iCloud 계정은 Apple Devloper account와 구별되지만, 같은 이메일을 사용할 수도 있다. 
    • 개발 도중에는 시뮬레이터에서도 테스트할 수 있도록, 시뮬레이터 - 설정 앱 - Sign in to your iPhone 선택하고, 아이디, 비밀번호, 입력하고, 정상적인지 기다린다. 그리고 다음으로 iCloud를 선택하고, iCloud Drive를 켠다. ( 만약 iCloud Drive가 없다면 이미 되고 있다는 뜻이다 ) 
  • 그리고, iCloud의 container들은 CloudKit Dashboard에서 확인할 수 있다. 
    • Cloudkit Dashboard를 더 알고 싶다면, 여기에서 확인할 수 있다.
    • 만약 dashBoard에서 컨테이너까지 나타나고, record를 쿼리 하는데, Field recordName is not marked queryable 이 뜬다면, 여기를 확인하라.  
      • 요약하면, index에서 recordName을 qurieable로 추가해야 한다.
    • ( 그리고 iCloud dashboard에서 record들이 안 나타난다면 조금 기다려보자! 5분 안에는 업데이트되어서 나타나더라!  )

 

 

CloutKit 세팅이 끝났다면, 

Background modes - Remote Notifications를 체크한다. 

  • 새로운 컨텐츠가 생겼을 때, 어떠한 알림 없이 조용하게 알리기 위해! 

 

 

만약 기존에 CoreData를 사용하고 있는 프로젝트에서 CloudKit을 추가하고자 하는 경우라면, 위와 같이, iCloud, CloudKit, push notification, remote notifications들을 모두 설정해주고, 

기존에 NSPersistentContainer를 사용했던 곳을 NSPersistentCloudKitContainer로 변경한다. 

  • NSPersistentContainer는 오로지 로컬 persistent store만 지원한다. 그러므로, local store를 CloudKit database와 동기화하기 위해서는 NSPersistentCloudKitContainer로 변경해야 한다.
  • ( 애초에 프로젝트를 생성할 때 Use CloudKit을 선택했다면 기본적으로 NSPersistentCludKitContainer로 코드가 생성되어있다. )

 

CloudKit에 있는 데이터들을 완전히 복제해서 분리된 채로 local에서 사용하고 싶을 수 있다.

그럴 때는, store를 여러 개로 둔다. 

  • Add Entity에서 Add Configuration을 클릭하면 Configurations에 뜨게 되고, 이제 원하는 Entity들을 드래그해서 넣는다.
  • 그리고 iCloud와 동기화하고자 하는 configuration을 선택해서 Used with CloudKit을 체크한다. 

  • 여기서 만약 configuration을 사용하지 않는다면, NSPersistentCloudKitContainer는 가장 첫 번째 store description을 CloudKit의 첫 번째 container identifer와 매칭 한다. 
  • 위에처럼 configuration을 사용한다면, CloudKit container에 말해주어야 하는데, 각 configuration마다 Description을 만들어야 한다. 
    • Description을 만들고, configuration을 지정한다.
    • 그리고 반드시 CloudKit과 동기화되어야 하는 configuration은 cloudKitContainerOptions를 contianer identifer로  지정한다. 
    • 그리고 persistent cotnainer를 로딩하기 전에 descripton에 추가해준다. 
    • ( 아래는 cloud store만 cloudKitContainerOptions를 지정했다 ) 

 

 

2. 그다음은 CloudKit과 잘 호환 가능한 CoreData model을 만들어야 한다. 

  • CloudKit은 Core Data model의 모든 특징들을 지원하지 않는다
  • 그러므로 그러한 제한을 인지하고 호환 가능한 Core Data model을 만들어야 한다.
    • Entities에서 Unique constraint를 지원하지 않는다.
    • Attributes에서 Undefined과 objectID attribute 타입을 지원하지 않는다. 
    • Relationship에서는 모든 relationship은 옵셔널이어야 한다.  
      • 또한 inverse를 가져야 한다.
      • Deny deletion rule를 지원하지 않는다. 
    • Configurations에서는 특정 configuration에 있는 Entity들의 relationship은 다른 configuration에 있는 entity와 relationship을 가지면 안 된다. 
  • 추가적으로 Core Data가 어떻게 CKRecord로 변환되는지 알고 싶다면 여기를 참고하라

 

 

 

 

3. 이렇게 다 끝났다면, 이제 동기화를 해보자. 

  • 앱을 실행하면 자동적으로 NSManagedObject 인스턴스가 생성되고, 자동적으로 CloudKit 데이터베이스와 동기화된다. 
  • 그리고 같은 iCloud 계정에 접속한 다른 디바이스들에게도 전파가 된다. 
  • 변경사항이 발생해도, 백그라운드에서 자동적으로 알아서 다 해준다. 
  • 그러므로 우리는 모든 디바이스들에 동기화되게끔 하기 위한 어떠한 코드도 필요가 없다. 

 

local에서 변화가 발생했을 때. 

  • ( 아래는 local에서 변경사항이 발생했고, 백그라운드에서 NSmanagedObject를 CKRecord로 변환해주고, CloudKit에 전달해준다. ) 

 

 

CloudKit에서 변화가 발생했을 때, 

  • ( 아래는 local에서 변화가 발생한 후, CloudKit에 업데이트된 후에, 다른 디바이스들에게 그 변화를 전파할 때 ) 
  • CloudKit은 각 다른 디바이스들에게 push notification을 보낸다.
  • 그다음에, 각 디바이스들에서는 시스템이 background task를 만들어 모든 변화된 Record들을 다운로드한다. 그리고 NSManagedObject로 변환한다.
  • 마지막에는 CoreData가 local에 저장한다. 

 

 

 

 

 


위와 같이 그대로 세팅하고, 에디터 모드에서 Add Configuration하지 않고,  NSPersistentCloudKitContainer로 변경하여 사용하면 

정말 신기하게도 Cloud랑 동기화되면서 다른 디바이스에서도 반영이 된다....!!

 

 

하지만 좀 더 궁금한 점이 생겨서 아래는 내가 그냥 정리한 글이다. 

( 혹시나 잘 못된 정보가 있다면 알려주세요!!🙏🏻)

 

 

Q1. store로 분리하면 특정 Store만 접근할 수 있는지? 

  • 아마, persistentStore를 각 store마다 load 하거나, 
  • 또는, model을 저장할 때, NSManagedObjectContext의 assign(_: to: PersistentStore )로 특정 store에 지정하거나. 
    • Q. 어떻게 assign to에 Persistentstore를 지정하지?  
      • 보면 coordinator를 이용하여 가져오라 한다.
      •  
      •   
      • let storeDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last! let localUrl = storeDirectory.appendingPathComponent("Local.sqlite") let store = self.container?.persistentStoreCoordinator.persistentStore(for: localUrl)​ self.context!.assign(comment, to: store!)
    • 오오오..각 store에 지정하여 assign하니까, 클라우드에만 저장이 되네!! 신기방기! 
      • 이로써, 각 하나의 Container에서 각기 다른 store를 불러와, local, cloud각각 따로 저장할 수 있다!!!
    • Q. 그런데, assign을 지정 안 하고 저장하면 local, cloud둘다 저장되어야 하는 거 아닌가? - ㅁ - 
      • When you save changes in a context, the changes are only committed “one store up.” If you save a child context, changes are pushed to its parent. Changes are not saved to the persistent store until the root context is saved. (A root managed object context is one whose parent context is nil.) In addition, a parent does not pull changes from children before it saves. You must save a child context if you want ultimately to commit the changes.   - 출처 NSManagedObjectContext  
        • parent까지 Save 했지만 cloud둘다 반영이 되지 않는걸?!
        • 아직까지 찾은 방법은, 한 번에 저장은 못하고, 각각 assign 해서 저장해야 하는 걸로 알고 있다. 
  • Q 그렇다면 하나의 container안에서 각기 다른 Store에서 가져오는 방법은? 
    •   request에서 특정 Store만 지정할 수 있다.  - affectedStores 
    • let request: NSFetchRequest<Comment> = Comment.fetchRequest()
      let localUrl = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("Cloud.sqlite")
      request.affectedStores = [self.container!.persistentStoreCoordinator.persistentStore(for: localUrl)!]​

 

 

Q2. store는 Add Configuration으로 추가한 것이 store가 되는 것인지? 

 

 

Q. store의 url은? 

  • default store의 url은  둘 중 하나, ( 둘 다 같음 ) 
  • // 1
    FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last!
    // 또는 2 
    NSPersistentContainer.defaultDirectoryURL()

 

Cloud의 변화를 디바이스에서 반영하기 위해서는, context.hasChange를 이용하거나,  remote change를 이용하거나, 여하튼, context의 Save를 한번 호출해야 한다. 

 

 

persistentContainer는 persistent store들을 생성하고 load 한다.

  • loadPersistentStores 를 호출하면, 각 persistent store마다 compeltion이 호출된다

 

coredata는 수천 개 이상의 데이터들을 관리하거나 가져오거나, 저장하거나, 하지 않는다면 충분히 main thread에서도 감당 가능하다. 

  • 그래서 collectionView, tableview에서 cellForRowAt에서 바로 fetch해도 문제가 없었음. 

 

 

 

 

 

 

 


참고

 

Setting Up Core Data With CloudKit

 

Apple Developer Documentation

 

developer.apple.com

Enabling CloudKit in Your App

 

Apple Developer Documentation

 

developer.apple.com

 

Creating Core Data Model for CloudKit

 

Apple Developer Documentation

 

developer.apple.com

 

Syncing a Core Data Store with CloudKit

 

Apple Developer Documentation

 

developer.apple.com

 

1. 다른 persistentStore에 다른 entities를 사용한다면,  2. 다른 persistent store에 같은 entities를 사용한다면, 

 

CoreData - multiple persistent stores

I need some help understanding CoreData. Let's say I have MagicalRecords and RestKit installed, and I also have a server, which can return me some posts. Then I need to save some of those posts o...

stackoverflow.com

 

'iOS' 카테고리의 다른 글

UICollectionView IndexPath 리팩토링  (0) 2021.08.28
RxSwift, RxCocoa 정리  (0) 2021.08.18
좌우 스크롤되는 캘린더뷰 만들기( feat: CompositionalLayout )  (0) 2021.08.09
각 주제별 WWDC 참고용  (0) 2021.07.09
WidgetKit 정리  (0) 2021.07.07