포스트

MapKit (6)

Profile Validation

프로필을 설정할때 유효성검사를 위해 함수를 만들어 준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
func isValidProfile() -> Bool {
    
    guard !firstName.isEmpty,
            !lastName.isEmpty,
            !companyName.isEmpty,
            !bio.isEmpty,
            avatar != PlaceholderImage.avatar,
            bio.count <= 100 else {
        return false
    }
    
    return true
}

그리고 키보드를 사라지게 하기 위해 view에 Extension으로 함수를 하나 만들어준다.

1
2
3
4
5
6
extension View {
    // 생략
    func dismissKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

그리고 toolbar에 아이콘을 만들어 준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ProfileView
Vstack {
    // 생략
    }
    .navigationTitle("Profile")
    .toolbar {
        Button {
            dismissKeyboard()
        } label: {
            Image(systemName: "keyboard.chevron.compact.down")
        }
    }
    .sheet(isPresented: $isShowingPhotoPicker) {
        PhotoPicker(image: $avatar)
    }

이 방식은 전에 프로젝트할때 키보드에 아이콘을 만들었던것과는 조금 다른 방식인데 그때는 UIkit을 사용했다.

이전글 참고

Image

그럼 이렇게 우측 상단에 만들어 진다.

이 강의가 iOS 14때라서 지금과는 다른 부분이 많다.

물론 이후에 버전에 맞게 수정하는 글도 올릴 예정

프로필 생성 함수 만들기

이전까지 프로필 생성 버튼만 있었다면 이젠 해당 버튼이 작동하게 만드는 함수를 만들어 본다

1
2
3
4
5
6
7
8
9
10
11
12
13
@State private var alertItem: AlertItem?

// LocationMapView의 Alert 복사해서 가져오기
.alert(item: $alertItem, content: { alertItem in
    Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
})

// AlertContext에 새로운 Item 추가.
// MARK: - ProfileView Errors
static let invalidProfile = AlertItem(title: Text("Invalid Profile"),
                                            message: Text("All fields are required as well as a profile photo. Your bio must be < 100 characters. \nPlease try again."),
                                            dismissButton: .default(Text("OK"))
)

그리고 함수를 만들고 버튼에 적용을 해주자

1
2
3
4
5
6
func createProfile() {
    guard isValidProfile() else {
        alertItem = AlertContext.invalidProfile
        return
    }
}

실행을해서 확인을 해보면

Image

이렇게 에러가 뜬다.

모든 내용을 채우고 이미지까지 업로드를 해야 에러가 사라진다.

Image

UIImage → CKAsset Conversion

이젠 우리가 프로필등록한걸 iCloud에 업로드를 하여 프로필을 저장하게 할건데, 이때 이미지는 일반적인 이미지타입이아닌 CkAsset으로 변환을 해야하기에 그 작업을 해보도록 한다.

이전에 CKAsset을 UIImage로 했던것과 유사 하다.

4단계 Step으로 나뉘어 진다.

  1. 문서 디렉터리의 URL 가져오기
  2. 파일 경로에 고유한 식별자 추가
  3. 이미지 데이터를 해당 위치에 저장
  4. 저장된 파일 URL을 사용해 CKAsset 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func convertToCKAsset() -> CKAsset? {
    
    // Get our apps base document directory url
    guard let urlPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
        print("Document Directory url came back nil")
        return nil
    }
    
    // Append some unique identifier for our profile image
    let fileURL = urlPath.appendingPathComponent("selectedAvatarImage")
    
    // Write the image data to the location the address points to
    guard let imageData = jpegData(compressionQuality: 0.25) else {
        return nil
    }
    
    // Create our CKAsset with that fileURL
    do {
        try imageData.write(to: fileURL)
        return CKAsset(fileURL: fileURL)
    } catch {
        return nil
    }
}

간단하게 스텝에 맞게 부연 설명을 적어보면

  1. 문서 디렉터리의 기본 URL 가져오기
    • FileManager를 사용하여 앱의 문서 디렉터리 경로를 가져온다.
    • 경로를 가져오지 못하면 nil을 반환한다.
  2. 파일 경로 지정
    • selectedAvatarImage라는 이름으로 파일을 저장할 경로를 생성한다.
  3. 이미지 데이터를 JPEG로 변환
    • jpegData(compressionQuality: 0.25)를 사용해 이미지 데이터를 압축하여 변환한다.
    • 변환에 실패하면 nil을 반환한다.
  4. 파일로 저장 후 CKAsset 생성
    • 변환된 이미지 데이터를 해당 경로에 저장한다.
    • 저장이 성공하면 CKAsset을 생성하여 반환하고, 실패하면 nil을 반환한다.

Profile Record 만들기

여기서 Record는 Cloud에서 사용하는 Record이다.

Image

바로 이것.

기존에 만들어둔 createProfile 함수는 현재 유효성만 검사를 하는데, 유효성 검사 이후 서버에(Cloud) 프로필을 업로드 하는 작업이 있어야하기에 이제 그작업을 해보려 한다.

여기는 5단계 Step으로 된다.

  1. CKRecord 생성
  2. UserRecordID 가져오기
  3. UserRecord 조회
  4. UserRecord에 프로필 참조 추가
  5. CloudKit 저장

Step 1

1
2
3
4
5
6
7
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()

여기서는 마지막 kAvatar는 CKAsset이므로 위에서 만들어둔 Extension을 활용하여 UIimage를 CKAsset으로 바꿔 주었다.

이건 예전에 Firebase와 했던것과 유사.

profileRecord는 CloudKit에서 RecordType.profile 타입을 가진 새로운 레코드이다.

Step 2

1
2
3
4
5
6
CKContainer.default().fetchUserRecordID { recordID, error in
    guard let recordID = recordID, error == nil else {
        print(error!.localizedDescription)
        return
    }
}

CKContainer.default().fetchUserRecordID를 사용하여 현재 사용자의 recordID를 가져온다.

이 ID는 CloudKit의 퍼블릭 데이터베이스에서 사용자의 기존 레코드를 찾는 데 사용된다.

  • 즉, 현재 로그인한 사용자의 recordID를 가져오는 과정이다.

Step 3

1
2
3
4
5
6
7
8
CKContainer.default().fetchUserRecordID { recordID, error in
    // Step 2 생략
    CKContainer.default().publicCloudDatabase.fetch(withRecordID: recordID) { userRecord, error in
        guard let userRecord = userRecord, error == nil else {
            print(error!.localizedDescription)
            return
        }
    }

가져온 recordID를 사용해 퍼블릭 데이터베이스에서 UserRecord를 가져온다. (이전에 Records를 Users, DDGLocation, DDGProfile 이렇게 3종류로 만들었다. - Users는 처음에 있었다.)

이 userRecord는 CloudKit에서 현재 로그인한 사용자의 기존 데이터(즉, CKRecord 객체)이다.

  • 하지만 처음 프로필을 만들 때는 userRecord가 존재하지 않기 때문에, CloudKit에 사용자 정보를 저장하는 방식에 따라 userRecord가 없을 수도 있다.
  • 일반적으로 앱에서는 최초 로그인 시 기본 userRecord를 생성하는 과정이 있어야 한다.

우리는 이전에 Users라는 생성된 레코드에 Fields를 추가한 적이 있다. (위의 사진 참조)

Step 4

1
2
3
4
CKContainer.default().publicCloudDatabase.fetch(withRecordID: recordID) { userRecord, error in
    // step 3 생략
    userRecord["userProfile"] = CKRecord.Reference(recordID: profileRecord.recordID, action: .deleteSelf)
}

가져온 UserRecord에 profileRecord를 참조(CKRecord.Reference) 형태로 추가한다.

Image

userProfile은 여기에!

CloudKit의 CKRecord는 자동으로 recordID를 할당하므로, profileRecord.recordID는 이 새 레코드의 고유 ID가 된다.

  • 즉, profileRecord.recordID는 새로 만든 프로필 레코드의 ID이다.

Step 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CKContainer.default().publicCloudDatabase.fetch(withRecordID: recordID) { userRecord, error in
    // step 4 생략
    let operation = CKModifyRecordsOperation(recordsToSave: [userRecord, profileRecord])
    operation.modifyRecordsCompletionBlock = { saveRecords, _, error in
        guard let saveRecords = saveRecords, error == nil else {
            print(error!.localizedDescription)
            return
        }
        
        print(saveRecords)
    }
    
    CKContainer.default().publicCloudDatabase.add(operation)
    
}

modifyRecordsCompletionBlock는 현재 deprecated 되었다.

CKModifyRecordsOperation을 사용하여 UserRecord와 profileRecord를 저장할 작업을 생성한다.

publicCloudDatabase.add(operation)을 호출하여 CloudKit 퍼블릭 데이터베이스에 추가한다.


이제 실행을해서 잘 되는지 확인을 해보자.

등록을 시도하니

Image

1
No iCloud account is configured

이런게 콘솔에 적힌다. icloud 로그인을 해주자.

로그인을하고 등록시도를 하니

1
Failed to modify some records

이런 메세지가 뜬다.

Image

하지만 등록이 되었다.

1
userRecord["userProfile"] = CKRecord.Reference(recordID: profileRecord.recordID, action: .none)

action을 deleteSelf에서 none으로 바꾸고 하니 제대로 작동이 된다.

action설명
.none참조된 레코드가 삭제되어도, 참조하는 레코드는 그대로 유지됨. (기본값)
.deleteSelf참조된 레코드가 삭제되면, 이를 참조하는 레코드도 함께 삭제됨.
  • .none → 레코드 간의 느슨한 관계 (참조된 레코드가 삭제되어도 영향 없음)
  • .deleteSelf → 강한 종속 관계 (참조된 레코드가 삭제되면 자신도 삭제됨)

일반적으로 .none을 기본으로 사용하며, 특정 경우에 .deleteSelf를 사용한다.

1
2
3
4
5
6
7
8
9
[<CKRecord: 0x1045d5640>
{
	생략
	firstName -> "Harold"
	lastName -> "Song"
	avatar -> <CKAsset: 0x149a6c3f0; path=~/Documents/selectedAvatarImage, size=28675, UUID=C7BB89A4-D9C2-480C-B895-170E0506E077, signature={length = 21, bytes = 0x017aaf1b04d64a88e929e748fe20b0dc02847145e4}>
	companyName -> "Test"
	bio -> "This is my bio"
}]

클라우드 사이트에도 잘 업로드가 되었다 (이미지는 생략)

이때 클라우드에 로그인하는 계정은 Developer가 아닌 계정이어도 된다. (왜냐면 처음에 CloudKit을 설정할때 container 주소 설정을 해뒀음.)

userProfile 필드에 값이 있는지 확인하고 싶지만 검색을 해도 안보여서 포기… 이부분은 나중에 알게되면 추후 서술하는걸로…


Github: Dub-Dub-Grub Repository

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