MapKit (8)
LoadingView 만들기
이전에는 ProgresHUD Library를 통해서 로딩하는 것을 표현했는데, 여기서는 별도의 View를 만들어서 관리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct LoadingView: View {
var body: some View {
ZStack {
Color(.systemBackground)
.opacity(0.9)
.ignoresSafeArea()
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .brandPrimary))
.scaleEffect(2)
.offset(y: -40)
}
}
}
그럼 이렇게 돌아가는걸 볼 수 있다.
profileViewModel로가서
1
2
3
4
5
6
7
8
9
@Published var isLoading = false
private func showLoadingView() {
isLoading = true
}
private func hideLoadingView() {
isLoading = false
}
loading의 상태를 알려주는 변수와 함수를 만들어 준다.
그리고 createProfile, 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
func createProfile() {
// 생략
userRecord["userProfile"] = CKRecord.Reference(recordID: profileRecord.recordID, action: .none)
showLoadingView() //new
CloudKitManager.shared.batchSave(records: [userRecord, profileRecord]) { result in
DispatchQueue.main.async { [self] in
hideLoadingView() // new
switch result {
case .success(_):
// show alert
break
case .failure(_):
// show alert
break
}
}
}
}
getProfile 함수에서도 위치는 동일하므로 코드 길이상 하나의 함수에만 적어둔다.
ProfileView에서도 적용을 해주자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ProfileView: View {
// 생략
var body: some View {
ZStack { // new
VStack {
// 생략
}
// new
if viewModel.isLoading {
LoadingView()
}
}
}
}
Zstack을 만들고 true / false에따라 LoadingView를 보여줄지 말지에 대해 정하면 된다.
실행하면 이렇게 로딩뷰가 보이고 사라지는걸 알 수 있다.
Alert 추가하기
AlertItem에서 Alert를 만들어 준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static let invalidProfile = AlertItem( // 생략
)
static let noUserRecord = AlertItem( // 생략
)
static let createProfileSuccess = AlertItem( // 생략
)
static let createProfileFailure = AlertItem( // 생략
)
static let unableToGetProfile = AlertItem( // 생략
)
그리고 이런식으로 AlertItem을 적용해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CloudKitManager.shared.batchSave(records: [userRecord, profileRecord]) { result in
DispatchQueue.main.async { [self] in
hideLoadingView()
switch result {
case .success(_):
alertItem = AlertContext.createProfileSuccess
case .failure(_):
alertItem = AlertContext.createProfileFailure
break
}
}
}
나머지는 생략!
Profile Update
1
2
3
4
5
6
7
8
9
10
11
// ViewModel
func save(record: CKRecord, completed: @escaping (Result<CKRecord, Error>) -> Void) {
CKContainer.default().publicCloudDatabase.save(record) { record, error in
guard let record = record, error == nil else {
completed(.failure(error!))
return
}
completed(.success(record))
}
}
프로필을 저장하는 함수를 만들어 준다.
그리고 그 프로필을 가지고있을 변수도 만들어 주고, 그 변수에 result가 success일경우 record를 해당 변수에 넣어준다.
무슨말이냐면
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
50
private var existingProfileRecord: CKRecord?
func createProfile() {
// 생략
showLoadingView()
CloudKitManager.shared.batchSave(records: [userRecord, profileRecord]) { result in
DispatchQueue.main.async { [self] in
hideLoadingView()
switch result {
case .success(let records):
for record in records where record.recordType == RecordType.profile { // new
existingProfileRecord = record
}
alertItem = AlertContext.createProfileSuccess
case .failure(_):
alertItem = AlertContext.createProfileFailure
break
}
}
}
}
func getProfile() {
// 생략
showLoadingView()
CloudKitManager.shared.fetchRecord(with: profileRecordID) { result in
DispatchQueue.main.async { [self] in
hideLoadingView()
switch result {
case .success(let record):
existingProfileRecord = record // new
let profile = DDGProfile(record: record)
firstName = profile.firstName
lastName = profile.lastName
companyName = profile.companyName
bio = profile.bio
avatar = profile.createAvatarImage()
case .failure(_):
// show alert
alertItem = AlertContext.unableToGetProfile
break
}
}
}
}
이렇게 existingProfileRecord record를 넣어주는데, createProfile만 for를 쓴 이유는, CloudKitManager.shared.batchSave(records: [userRecord, profileRecord])
여기서 records에 userRecord, profileRecord 두개의 값이 들어가기 때문.
우리는 profileRecord만 필요하기에 해당부분만 빼서 넣어주는 것이다.
이렇게 했으면 updateProfile 함수를 만들어 본다. (이때 아까 만든 save 가 사용된다.)
그리고 AlertItem도 추가해주자
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
func updateProfile() {
guard isValidProfile() else {
alertItem = AlertContext.invalidProfile
return
}
guard let profileRecord = existingProfileRecord else {
alertItem = AlertContext.unableToGetProfile
return
}
profileRecord[DDGProfile.kFirstName] = firstName
profileRecord[DDGProfile.kLastName] = lastName
profileRecord[DDGProfile.kCompanyName] = companyName
profileRecord[DDGProfile.kBio] = bio
profileRecord[DDGProfile.kAvatar] = avatar.convertToCKAsset()
showLoadingView()
CloudKitManager.shared.save(record: profileRecord) { result in
DispatchQueue.main.async { [self] in
hideLoadingView()
switch result {
case .success(_):
alertItem = AlertContext.updateProfileSuccess
case .failure(_):
alertItem = AlertContext.updateProfileFailure
}
}
}
}
// AlertItem
static let updateProfileSuccess = AlertItem( // 생략
)
static let updateProfileFailure = AlertItem( // 생략
)
Enum을 통한 create, update 분류
지금 ViewModel에는 ProfileRecord를 저장하는 관련 함수는 Create, Update 2개가 있다.
이걸 Enum을 통해 각 케이스에 맞게 버튼 및 함수를 다르게 작동하도록 바꿔본다.
1
2
3
enum ProfileContext {
case create, update
}
우선 이렇게 2가지 케이스에 대해 만들어 준다.
그리고 didSet을 사용하여 existingProfileRecord의 값이 변할때 profileContext가 update로 되도록 바꿔준다.
1
2
3
private var existingProfileRecord: CKRecord? {
didSet { profileContext = .update }
}
이후 버튼을 수정 해준다.
이때 3항연산자를 사용하여 조건에 맞게 create냐 update냐로 해주면 된다.
1
2
3
4
5
6
7
// ProfileView
Button {
viewModel.profileContext == .create ? viewModel.createProfile() : viewModel.updateProfile()
} label: {
DDGButton(title: viewModel.profileContext == .create ? "Create Profile" : "Update Profile")
}
이제 실행을 해보면
처음에 앱을 새로 설치하고 프로필을 만들어야하는 첫화면
프로필을 설정하고 이후의 화면
이렇게 2개의 화면으로 버튼이 다르게 표시되는걸 알 수 있다.
didSet
을 사용한 이유
프로필을 처음 생성할 때는 existingProfileRecord
가 nil
이므로
profileContext = .create
상태이다.
그러나 사용자가 기존 프로필을 불러오면, existingProfileRecord
에 값이 설정되면서
버튼의 상태가 "Create Profile"
→ "Update Profile"
로 변경되어야 한다.
이를 자동으로 처리하기 위해 didSet
을 사용하여
existingProfileRecord
값이 변경될 때마다 profileContext
를 .update
로 설정하도록 했다.
앱의 흐름
- 앱이 실행되면
AppTabView
에서CloudKitManager.getUserRecord()
를 호출 - 유저가 ProfileView로 진입하면
ViewModel.getProfile()
이 실행됨 getProfile()
내부에서userRecord
를 가져와fetchRecord()
를 호출하여- ✅
existingRecord
에 값이 있으면 →profileContext = .update
- ❌
existingRecord
가 없다면 →profileContext = .create
- ✅
didSet
을 사용함으로써 얻는 이점
- ✅ 값이 변경될 때 자동으로 상태 업데이트
existingProfileRecord
가 변경될 때마다profileContext
를.update
로 설정- 별도의 추가 로직 없이 버튼 상태를 자동으로 변경 가능
- ✅ UI와 데이터 흐름이 자연스럽게 연결됨
- 프로필이 없을 때만
"Create Profile"
버튼이 표시되며,
프로필이 있으면"Update Profile"
버튼이 자동으로 적용됨
- 프로필이 없을 때만
📝 정리
didSet
을 사용하면existingProfileRecord
값이 변경될 때마다
profileContext
가 자동으로.update
로 설정됨.- 이를 통해 ViewModel과 View가 동기화되며, 버튼의 상태를 자동으로 변경할 수 있음.
Github: Dub-Dub-Grub Repository