포스트

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)
        }
    }
}

Image

그럼 이렇게 돌아가는걸 볼 수 있다.

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를 보여줄지 말지에 대해 정하면 된다.

Image

실행하면 이렇게 로딩뷰가 보이고 사라지는걸 알 수 있다.

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")
}

이제 실행을 해보면

처음에 앱을 새로 설치하고 프로필을 만들어야하는 첫화면

Image

프로필을 설정하고 이후의 화면

Image

이렇게 2개의 화면으로 버튼이 다르게 표시되는걸 알 수 있다.

didSet을 사용한 이유

프로필을 처음 생성할 때는 existingProfileRecordnil이므로
profileContext = .create 상태이다.
그러나 사용자가 기존 프로필을 불러오면, existingProfileRecord에 값이 설정되면서
버튼의 상태가 "Create Profile""Update Profile"로 변경되어야 한다.

이를 자동으로 처리하기 위해 didSet을 사용하여
existingProfileRecord 값이 변경될 때마다 profileContext.update로 설정하도록 했다.


앱의 흐름

  1. 앱이 실행되면 AppTabView에서 CloudKitManager.getUserRecord()를 호출
  2. 유저가 ProfileView로 진입하면 ViewModel.getProfile()이 실행됨
  3. 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

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.