MapKit (7)
Record 값 가져오기
이전글에서 Cloud에 등록을 했는데, 이젠 등록한 값을 가져오는 작업을 해보자.
getProfile이라는 함수를 만들어 주었다.
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
40
41
42
43
44
func getProfile() {
CKContainer.default().fetchUserRecordID { recordID, error in
guard let recordID = recordID, error == nil else {
print(error!.localizedDescription)
return
}
CKContainer.default().publicCloudDatabase.fetch(withRecordID: recordID) { userRecord, error in
guard let userRecord = userRecord, error == nil else {
print(error!.localizedDescription)
return
}
let profileReference = userRecord["userProfile"] as! CKRecord.Reference
let profileRecordID = profileReference.recordID
CKContainer.default().publicCloudDatabase.fetch(withRecordID: profileRecordID) { profileRecord, error in
guard let profileRecord = profileRecord, error == nil else {
print(error!.localizedDescription)
return
}
DispatchQueue.main.async {
let profile = DDGProfile(record: profileRecord)
firstName = profile.firstName
lastName = profile.lastName
companyName = profile.companyName
bio = profile.bio
avatar = profile.createAvatarImage()
}
}
}
}
}
// DDGProfile
func createAvatarImage() -> UIImage {
guard let avatar = avatar else {
return PlaceholderImage.avatar
}
return avatar.convertToUIImage(in: .square)
}
- fetchUserRecordID()를 사용하여 현재 사용자의 recordID를 가져온다.
- publicCloudDatabase.fetch(withRecordID: recordID)를 사용하여 UserRecord를 가져온다.
- userRecord[“userProfile”] 필드에서 CKRecord.Reference 값을 가져온다.
- 참조된 profileRecordID를 사용해 다시 profileRecord를 가져온다.
- 이때 UI Update를 하므로 DispatchQueue를 사용하여 Main Thread에서 이루어지도록 한다.
잘 가져오는걸 확인할 수 있다. (이전글과 다른 프로필 내용인건 이전에 create를 2번하여 최신걸 지우니 제대로 못가져오는 에러가 발생하여 새로 다시 프로필을 업로드함)
Refactoring (ViewModel)
기존 코드에서 CloudKit 관련 작업이 ProfileViewModel에 섞여 있어 유지보수가 어려웠다.
이를 해결하기 위해 CloudKit 관련 코드를 CloudKitManager로 분리하고, ViewModel에서는 UI 상태 관리만 담당하도록 리팩토링했다.
- 기존 문제점
- ProfileViewModel에 CloudKit 관련 코드가 많아 역할이 명확하지 않음
- CloudKit 관련 코드를 재사용하기 어려움
- 리팩토링 방식
- CloudKitManager: CloudKit 관련 작업 (
fetch
,batchSave
,getUserRecord
)을 담당 - ProfileViewModel: UI 상태 (
firstName
,lastName
,bio
,avatar
)를 관리 - CloudKitManager를 싱글턴(
static let shared = CloudKitManager()
)으로 만들어 어디서든 동일한 인스턴스를 사용할 수 있도록 변경
- CloudKitManager: CloudKit 관련 작업 (
ProfileViewModel을 만들어준다.
여기에 ProfileView에 만들었던 함수들을 옮겨줄것이다.
그리고 ProfileView에서 사용했던 변수들도 다 옮겨주는데 이때 한가지 팁이 있다.
ProfileView에선 @State로 Property Wrapper를 사용했는데, ViewModel로 옮기면서는 Published로 바꿔줘야하는데, 이걸 하나하나 다 바꾸려면 번거롭다.
이때 옵션을 누른채로 드래그를 하면 드래그 한 영역에 대해 일괄적인 수정이 가능하다.
아래 사진을 참고!
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
final class ProfileViewModel: ObservableObject {
@Published var firstName = ""
@Published var lastName = ""
@Published var companyName = ""
@Published var bio = ""
@Published var avatar = PlaceholderImage.avatar
@Published var isShowingPhotoPicker = false
@Published var alertItem: AlertItem?
func isValidProfile() -> Bool {
// 생략
}
func createProfile() {
// 생략
}
func getProfile() {
// 생략
DispatchQueue.main.async { [self] in // modified
let profile = DDGProfile(record: profileRecord)
firstName = profile.firstName
lastName = profile.lastName
companyName = profile.companyName
bio = profile.bio
avatar = profile.createAvatarImage()
}
}
profileView에 이제 변수가 없어서 에러가 뜨는데
@StateObject private var viewModel = ProfileViewModel()
viewmodel 변수를 만들고 viewmodel에서 변수를 가져오게 적용하면 끝.
1
2
3
4
5
// Example
ZStack {
AvatarView(image: viewModel.avatar, size: 84)
EditImage()
}
그리고
1
2
3
4
5
6
7
8
9
10
private func createProfileRecord() -> CKRecord {
let profileRecord = CKRecord(recordType: RecordType.profile)
profileRecord[DDGProfile.kFirstName] = firstName
profileRecord[DDGProfile.kLastName] = lastName
profileRecord[DDGProfile.kCompanyName] = companyName
profileRecord[DDGProfile.kBio] = bio
profileRecord[DDGProfile.kAvatar] = avatar.convertToCKAsset()
return profileRecord
}
profileRecord를 만드는 것도 함수로 관리를 해준다.
기존에 createProfile함수에 있던 내용을 위의 함수로 대체하고
let profileRecord = createProfileRecord()
이렇게 간단하게 한줄로 바꿔준다.
CloudKitManager Revamp
CloudKitManager를 struct에서 final class로 바꿔준다.
이유는 싱글턴을 사용해서 CloudKitManager를 사용할것이기 때문.
CloudKitManager를 싱글턴을 적용한 이유는 모든 CloudKit 연산을 하나의 인스턴스로 관리하기 위함이다.
그리고 structure에는 값을 저장 할 수 없고, class는 가능한 부분도 있다. (물론 mutating을 사용하면 이야기가 달라지긴 한다.)
1
2
static let shared = CloudKitManager()
private init() {}
이부분에 대한 설명은 패스. 싱글턴을 사용하다보면 너무나 유명한 코드.
userRecord를 가져오는 함수를 만들고 viewmodel에 있던 코드를 일부 가져오기만 하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func getUserRecord() {
CKContainer.default().fetchUserRecordID { recordID, error in
guard let recordID = recordID, error == nil else {
print(error!.localizedDescription)
return
}
CKContainer.default().publicCloudDatabase.fetch(withRecordID: recordID) { userRecord, error in
guard let userRecord = userRecord, error == nil else {
print(error!.localizedDescription)
return
}
self.userRecord = userRecord // new
print(self.userRecord)
}
}
}
그리고 새롭개 만든 변수 userRecord에 값을 넣어주면 끝.
userRecord를 잘 가져오는지 확인 하기 위해 Tabview 코드에 onAppear를 통해 확인을 해본다.
print를 통해 잘 출력이 되는지만 확인하면 된다.
1
2
3
.onAppear {
CloudKitManager.shared.getUserRecord()
}
실행해보면
1
2
Optional(<CKRecord: 0x101e77960>
{ 생략
잘 출력이 된다.
getProfile에 있던 CloudKit관련 코드들을
CloudKitManager에 새로 만들어 준다.
1. batchSave
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
40
41
42
43
44
45
46
47
48
49
// CloudKitManager
func batchSave(records: [CKRecord], completed: @escaping (Result<[CKRecord], Error>) -> Void) {
let operation = CKModifyRecordsOperation(recordsToSave: records)
operation.modifyRecordsCompletionBlock = { saveRecords, _, error in
guard let saveRecords = saveRecords, error == nil else {
print(error!.localizedDescription)
completed(.failure(error!))
return
}
completed(.success(saveRecords))
}
CKContainer.default().publicCloudDatabase.add(operation)
}
// ProfileViewModel
func getProfile() {
guard let userRecord = CloudKitManager.shared.userRecord else {
return
}
guard let profileReference = userRecord["userProfile"] as? CKRecord.Reference else {
return
}
let profileRecordID = profileReference.recordID
CloudKitManager.shared.fetchRecord(with: profileRecordID) { result in
DispatchQueue.main.async { [self] in
switch result {
case .success(let record):
let profile = DDGProfile(record: record)
firstName = profile.firstName
lastName = profile.lastName
companyName = profile.companyName
bio = profile.bio
avatar = profile.createAvatarImage()
case .failure(_):
// show alert
break
}
}
}
}
Escaping Closure를 사용해서 만들어 준다.
그리고 profileViewmodel에 적용 해주면 된다.
예전에 파이널 프로젝트할떄도 이런식으로 리팩토링을 한적이 있기에 크게 어려운점은 없는듯하다.
2. fetchRecord
같은 방법으로 만들어 준다.
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
40
41
42
43
44
45
46
func fetchRecord(with id: CKRecord.ID, completed: @escaping (Result<CKRecord, Error>) -> Void) {
CKContainer.default().publicCloudDatabase.fetch(withRecordID: id) { record, error in
guard let record = record, error == nil else {
completed(.failure(error!))
return
}
completed(.success(record))
}
}
func getProfile() {
guard let userRecord = CloudKitManager.shared.userRecord else {
return
}
guard let profileReference = userRecord["userProfile"] as? CKRecord.Reference else {
return
}
let profileRecordID = profileReference.recordID
CloudKitManager.shared.fetchRecord(with: profileRecordID) { result in
DispatchQueue.main.async { [self] in
switch result {
case .success(let record):
let profile = DDGProfile(record: record)
firstName = profile.firstName
lastName = profile.lastName
companyName = profile.companyName
bio = profile.bio
avatar = profile.createAvatarImage()
case .failure(_):
// show alert
break
}
}
}
}
1,2 둘다 아직은 Alert 부분은 제외하고 리팩토링만 진행을 했다.
프로필을 지우고 새로 만들어 테스트를 해보았는데, 작동이 잘되었다. (이미지는 생략!)
Github: Dub-Dub-Grub Repository