MapKit (13)
클라우드에 새로운 Field 추가하기
CloudKit 대시보드에서 새로운 데이터 필드를 추가하고, 이를 검색 가능하도록 설정하는 과정을 정리해본다.
우선 isCheckedInNilCheck 라는 필드를 만들어 준다. 말그대로 체크인이 NIL인지 아닌지를 확인하는 필드이다.
Boolean이 없기때문에 아주 기본적인 방법 0,1로 true/false를 구분할것이다.
또한 Queryable이 필요하므로 우선 저장부터한뒤, Queryable 가능하도록 추가해준다. 저장을 먼저 해주는 이유는 필드를 선택해야 하기 때문.
사진을 여러개 하는거보다 이렇게 움짤로 과정을 올리는게 편할듯 해보여서 움짤로 두 과정을 간단하게 정리한다.
Xcode에 적용하기
그리고 다시 Xcode로 돌아와서 해당 필드를 추가해주자 static let kIsCheckedInNilCheck = "isCheckedInNilCheck"
그리고 관련된 VM에 해당 부분을 적용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Location
func updateCheckInStatus(to checkinStatus: CheckInStatus) {
// 생략
CloudKitManager.shared.fetchRecord(with: profileRecordID) { [self] result in
switch result {
case .success(let record):
// Create a reference to the location
switch checkinStatus {
case .checkedIn:
record[DDGProfile.kIsCheckedIn] = CKRecord.Reference(recordID: location.id, action: .none)
record[DDGProfile.kIsCheckedInNilCheck] = 1 // added
case .checkedOut:
record[DDGProfile.kIsCheckedIn] = nil
record[DDGProfile.kIsCheckedInNilCheck] = nil // added
}
}
// 생략
}
}
// Profile
func checkOut() {
// 생략
CloudKitManager.shared.fetchRecord(with: profileID) { result in
switch result {
case .success(let record):
record[DDGProfile.kIsCheckedIn] = nil
record[DDGProfile.kIsCheckedInNilCheck] = nil
// 생략
}
}
}
체크인을 하게되면 1을 추가했고 체크아웃을 했을시엔 nil로 해주었다. 물론 0으로 해도 되지만 강의에선 nil로 해주었다.
실행해서 제대로 작동하는지 확인을 해보도록 한다.
체크인을 했다면 cloud 페이지에서 1이 떠야하고 체크아웃을 했다면 cloud 페이지에 아무값도 없어야 한다.
reload를 하면 nilcheck 부분이 사라지지 않아서 records쪽에서 새로고침을 하면 바로 보인다.
확인해보면 클라우드에도 잘 적용이 되는걸 알 수 있다.
체크인 중인 데이터 가져오기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func getCheckedInProfilesDictionary(completed: @escaping (Result<[CKRecord.ID: [DDGProfile]], Error>) -> Void) {
let predicate = NSPredicate(format: "isCheckedInNilCheck == 1")
let query = CKQuery(recordType: RecordType.profile, predicate: predicate)
let operation = CKQueryOperation(query: query)
// operation.desiredKeys = [DDGProfile.kIsCheckedIn, DDGProfile.kAvatar]
var checkedInProfiles: [CKRecord.ID: [DDGProfile]] = [:]
operation.recordFetchedBlock = { record in
// Build our dictionary
let profile = DDGProfile(record: record)
guard let locationReference = profile.isCheckedIn else { return }
checkedInProfiles[locationReference.recordID, default: []].append(profile)
}
operation.queryCompletionBlock = { cursor, error in
guard error == nil else {
completed(.failure(error!))
return
}
// handle cursor in later video
completed(.success(checkedInProfiles))
}
CKContainer.default().publicCloudDatabase.add(operation)
}
// DDGProfile
let isCheckedIn: CKRecord.Reference? // modified
init(record: CKRecord) {
// 생략
isCheckedIn = record[DDGProfile.kIsCheckedIn] as? CKRecord.Reference
}
사실 위의 predicate, query, operation 부분은 이전에도 많이 언급을 해서 패스한다.
다만 operation.desiredKeys = [DDGProfile.kIsCheckedIn, DDGProfile.kAvatar] 이 부분에 대해서 짧게 언급을 하자면
현재 isCheckedInNilCheck == 1 인 값을 모두 가져오는데, 위의 operation.desiredKeys 를 사용하게 되면 해당 값을 가져오되 kIsCheckedIn, kAvatar 부분만 가져오게 되므로 데이터 다운에 있어 좀 더 효율적으로 할 수 있게 된다. 한마디로 정리하면 데이터 최적화
그리고 DDGProfile에서 기존에 isCheckedIn 부분이 원래는 let isCheckedIn: CKRecord.Reference? = nil 로 처음부터 nil로 해주었었는데, 이제는 init 에 넣어주었다.
이 방식은 객체를 만드는 시점에 클라우드에 저장된 값을 프로퍼티에 바로 연결하는 데이터 매핑(Mapping) 과정이다. Firebase에서 스냅샷으로 초기화하는 것과 같은 원리로, 객체가 생성될 때 필요한 값이 모두 결정되기 때문에 코드가 더 명확해지고 데이터가 꼬일 위험도 줄어든다. 결과적으로 백엔드 데이터와 앱 내 모델을 하나로 일치시키는 효율적인 구조가 된다.
다시 함수로 돌아와서
recordFetchedBlock -> 현재는 Deprecated, 일단 이 시점에서 이건 해당 데이터를 조회하는 역할로 보면 된다. 말그대로 fetch
checkedInProfiles[locationReference.recordID, default: []].append(profile) 이부분은
처음에 checkedInProfiles 딕셔너리가 비어있기 때문에 key인 locationReference.recordID와 value에 아무것도 없는 [] 를 넣어준것이라고 보면된다. 그 딕셔너리에 profile이 추가 되는것.
조금 더 이해 쉽게 약간의 부연설명을 보태면
- .append 직전, 0.0001초 동안의 상태
- 탐색: checkedInProfiles[locationReference.recordID]를 먼저 찾아본다.
- 부재 확인: “어? 이 키(장소 ID)로 등록된 값이 없네?”라고 판단한다.
- 기본값 생성: 이때 default: []가 작동하면서 [locationReference.recordID: []] 라는 상태를 메모리에 임시로 혹은 즉시 생성한다.
- 추가: 바로 그 직후에 기다리고 있던 .append(profile)가 그 빈 배열([]) 안으로 들어간다.
- 정리: [:] → [ID: []] (default 작동) → [ID: [Profile]] (append)
물론 default는 두가지 용도로 사용이 되긴 한다.
데이터를 넣을 때 (Write): 존재하지 않는 키에 데이터를 추가해야 할 때, 에러 없이 즉석에서 ‘기본 저장소’를 만들어주는 역할을 한다. (예: 빈 배열 생성 후 append)
데이터를 읽을 때 (Read): 찾으려는 키가 없을 때
nil이 나오는 것을 방지하고, 미리 정해둔 ‘대체 값’을 반환하여 코드의 안정성을 높인다. (예: HTTP 301 에러 대처 Dictionary Default Docs 출처)
queryCompletionBlock -> 역시나 현재는 Deprecated, 이건 fetch 이후 데이터를 download하는 과정으로 이해하면 쉽다.
이때 한가지 알아둘게 있는데 CloudKit의 경우 한번에 리턴할수 있는 값을 정할 수 있다.
가져온 데이터 확인
위에서 작성한 코드가 제대로 작동하는지 확인을 하는 과정이 필요하기에 딕셔너리를 콘솔에 출력 해보려고 한다.
1
2
3
4
5
6
7
8
9
10
.onAppear {
CloudKitManager.shared.getCheckedInProfilesDictionary { result in
switch result {
case .success(let checkedInProfiles):
print(checkedInProfiles)
case .failure(_):
print("Error getting back dictionary")
}
}
}
일단은 print를 통해 데이터를 가져오는지를 확인해본다.
체크인 상태에서 LocationList로 가게되면
1
2
3
[<CKRecordID: 0x6000004d8720;
// 생략
recordName=434EEF9C-32FF-401E-9CE4-87315B85C934, zoneID=_defaultZone:__defaultOwner__>"]
이런식으로 값이 프린팅 되는걸 알 수 있다.
혹시라도 콘솔에 출력이 안된다면?
CKContainer.default().publicCloudDatabase.add(operation) 이게 반드시 들어가야 하기에 꼭 넣도록 하자.
우리가 지금까지 만든 operation을 최종적으로 클라우드db에 추가해주는 작업이기 때문이다.
Github: Dub-Dub-Grub Repository