- CloudKit의 기본적인 기능 구현을 익히는 데에 도움을 주는 Bare-bones 프로젝트입니다.
- CloudKit을 통해 SwiftUI 기반의 (사진과 함께 일기를 작성하고 공유할 수 있는) 포토 다이어리를 구현합니다.
- CloudKit을 처음 활용해 보는 경우, 이 프로젝트의 코드를 살펴보면 도움이 됩니다.
CloudKit_BareBones.mp4
iCloud를 기반으로 사용자의 iCloud 계정을 통해 앱 데이터를 저장하고 공유할 수 있는 Apple의 원격 데이터 저장 서비스입니다.
많은 데이터를 사용하지만, 많은 양의 서버 측 로직이 필요하지 않은 iOS 전용 앱에서 사용하기 좋습니다.
CloudKit을 사용하려면, 우선 iCloud 연동 및 CloudKit Console 초기 세팅에 대해 살펴보세요.
Database의 종류
CloudKit에는 3가지 종류의 데이터베이스가 있고, 상황에 따라 적합한 것을 사용할 수 있습니다.
- Public Database : 앱의 공개 데이터베이스
- Private Database : 사용자의 개인 데이터베이스
- Shared Database : 공유 데이터를 포함하는 데이터베이스
→ CloudKit_Barebones 프로젝트에서는 Public Database를 사용합니다.
CloudKit Database에서 데이터를 불러오고(fetch), 추가하고(upload), 수정하고(update), 삭제합니다(delete)
CloudKit과 상호작용하는 핵심적인 코드를 참고하세요.
// 데이터 불러오기
@Published var diaries = [Diary]()
func fetchDiary() {
var diaries: [Diary] = []
// Diary 데이터를 불러오기 위한 질의 작성
let query = CKQuery(recordType: RecordType.diary.rawValue, predicate: NSPredicate(value: true))
query.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] // 날짜를 기준으로 내림차순 정렬 (최신순)
CKContainer.default().publicCloudDatabase.fetch(withQuery: query) { result in
switch result {
case .success(let result):
result.matchResults.compactMap { $0.1 }
.forEach {
switch $0 {
case .success(let record):
diaries.append(Diary(record: record))
case .failure(let error):
print(error)
}
}
DispatchQueue.main.async {
// 불러온 데이터를 '@Published 프로퍼티 래퍼를 사용해서 선언된 변수 diaries'에 넣기
// 참고) @Published를 사용해서 선언된 변수의 값이 변경되면, 해당 변수를 사용한 모든 뷰의 내용이 자동으로 변경된다.
self.diaries = diaries
}
case .failure(let error):
print(error)
}
}
}
// 데이터 추가하기
func uploadDiary(photo: UIImage, title: String, content: String, completion: @escaping (Bool) -> Void) {
let record = CKRecord(recordType: RecordType.diary.rawValue)
// 이미지 타입을 UIImage에서 CKAsset으로 변경 후, 업로드할 레코드에 setValue
guard let imageData = photo.jpegData(compressionQuality: 1.0) else { return }
let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("photo")
do {
try imageData.write(to: url!)
let asset = CKAsset(fileURL: url!)
record.setValue(asset, forKey: "photo")
} catch {
print(error)
}
// 날짜, 제목, 콘텐츠 데이터를 업로드할 레코드에 setValue
record.setValue(Date(), forKey: "date")
record.setValue(title, forKey: "title")
record.setValue(content, forKey: "content")
CKContainer.default().publicCloudDatabase.save(record) { newRecord, error in
if let error = error {
print(error)
completion(false)
} else {
if let newRecord = newRecord {
DispatchQueue.main.async {
// 새로운 데이터를 '@Published 프로퍼티 래퍼를 사용해서 선언된 변수 diaries' 맨 앞에 추가하기
self.diaries.insert(Diary(record: newRecord), at: 0)
}
completion(true)
}
}
}
}
fetchDiary()
를 사용하지 않고, ‘@Published 변수 diaries’에 추가해 줌으로써 새로운 레코드를 화면에 불러오는 이유는 :
새로운 레코드가 추가된 즉시 화면에 바로 업데이트하기 위함입니다..fetchDiary()
를 호출하면, 새로운 레코드가 CloudKit DB에 저장되지도 않았는데
데이터를 불러오는 상황이 되어 추가된 레코드가 화면에 바로 반영되지 않습니다 🚨
// 데이터 수정하기
func updateDiary(id: CKRecord.ID, photo: UIImage, title: String, content: String, completion: @escaping (Bool) -> Void) {
// 일단 업데이트할 레코드 하나 불러오기
CKContainer.default().publicCloudDatabase.fetch(withRecordID: id) { updatedRecord, error in
if let error = error {
print(error)
}
// 이미지 타입을 UIImage에서 CKAsset으로 변경 후, 업데이트할 레코드에 setValue
guard let imageData = photo.jpegData(compressionQuality: 1.0) else { return }
let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("photo")
do {
try imageData.write(to: url!)
let asset = CKAsset(fileURL: url!)
updatedRecord?.setValue(asset, forKey: "photo")
} catch {
print(error)
}
// 제목, 콘텐츠 데이터를 업데이트할 레코드에 setValue (업데이트할 때 date는 변경되지 않아요)
updatedRecord?.setValue(title, forKey: "title")
updatedRecord?.setValue(content, forKey: "content")
CKContainer.default().publicCloudDatabase.save(updatedRecord!) { newRecord, error in
if let error = error {
print(error)
completion(false)
} else {
if let updatedRecord = updatedRecord {
DispatchQueue.main.async {
// @Published 변수인 diaries의 요소 중 업데이트한 레코드와 id가 일치하는 레코드를 찾아,
// 해당 레코드를 업데이트한 레코드로 변경하기
for (index, diary) in self.diaries.enumerated() {
if diary.id == updatedRecord.recordID {
self.diaries[index] = Diary(record: updatedRecord)
}
}
}
completion(true)
}
}
}
}
}
- 여기서
fetchDiary()
를 호출하지 않고, self.diaries[index]를 활용한 이유도 ‘데이터 추가하기’ 부분에서 설명한 이유와 같습니다.
// 데이터 삭제하기
func deleteDiary(id: CKRecord.ID, completion: @escaping (Bool) -> Void) {
CKContainer.default().publicCloudDatabase.delete(withRecordID: id) { deletedRecordId, error in
if let error = error {
print(error)
completion(false)
} else {
completion(true)
}
}
}
- 데이터를 삭제하는 경우에는 upload 및 update와 달리 CloudKit에 삭제가 반영된 이후에
fetchDiary()
가 호출되기 때문에,fetchDiary()
를 사용해도 삭제한 레코드가 화면에서 즉시 사라집니다.
앱의 서버 활동을 보고, 컨테이너를 관리하고, 데이터베이스 스키마를 유지하고, 테스트 데이터를 보고 편집할 수 있는 직관적인 웹 기반 제어판입니다.
CloudKIt Console에서 살펴 볼 수 있는, CloudKit Database의 예시입니다. (해당 프로젝트의 데이터베이스 사진입니다.)