[SwiftUI] CloudKit: CRUD

Junyoung Park·2022년 8월 24일
0

SwiftUI

목록 보기
50/136
post-thumbnail

CloudKit CRUD Functions in SwiftUI project | Advanced Learning #22

CloudKit: CRUD

구현 목표

  • iCloud 데이터 CRUD 함수 구현
  • 데이터 생성
  • 데이터 읽기
  • 데이터 업데이트
  • 데이터 삭제

핵심 코드

    private func saveItem(record: CKRecord) {
        CKContainer.default().database(with: .public).save(record) { returnedRecord, returnedError in
            print("Record: \(String(describing: returnedRecord))")
            print("Error: \(String(describing: returnedError))")
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
                guard let self = self else { return }
                self.text = ""
                self.fetchItems()
            }
        }
    }
  • CKContainer를 통해 클라우드 킷 데이터베이스 접근, 생성한 레코드를 곧바로 저장(딕셔너리 키-값 매칭 이후) 가능
  • 데이터 저장 이후 옵저버를 통해 UI 패치 가능 → 현 상태에서는 1초 딜레이를 통해 의도적으로 UI 패치가 데이터 저장이 완료된 이후에 되도록 구현
func fetchItems() {
        let predicate = NSPredicate(value: true)
        let query = CKQuery(recordType: "Fruits", predicate: predicate)
        query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        let queryOperation = CKQueryOperation(query: query)
        var returnedItems: [FruitModel] = []
        
        if #available(iOS 15, *) {
            queryOperation.recordMatchedBlock = { (returnedRecordId, returnedResult) in
                switch returnedResult {
                case .success(let record):
                    guard let name = record["name"] as? String else { return }
                    returnedItems.append(FruitModel(name: name, record: record))
                    print(name)
                    break
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        } else {
            queryOperation.recordFetchedBlock = { returnedRecord in
                guard let name = returnedRecord["name"] as? String else { return }
                returnedItems.append(FruitModel(name: name, record: returnedRecord))
            }
        }
        if #available(iOS 15, *) {
            queryOperation.queryResultBlock = { [weak self] returnedResult in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    self.fruits = returnedItems
                }
                print("RETURNED RESULT: \(returnedResult)")
            }
        } else {
            queryOperation.queryCompletionBlock = { [weak self] (returnedCursor, returnedError) in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    self.fruits = returnedItems
                }
                print("RETURNED queryCompletionBlock")
            }
        }
        addOperation(operation: queryOperation)
    }
  • 쿼리 연산을 수행하기 위한 쿼리 작성 필요
  • 레코드 타입, 레코드를 찾는 술어, 정렬 방법 등 쿼리 생성
  • iOS 타입에 따라 서로 다른 클라우드 데이터베이스 접근 방법
  • 레코드 값을 통해 로컬 변수 모델링 → 비동기 데이터 약한 참조 및 메인 스레드 UI 업데이트 주의

소스 코드

import SwiftUI
import CloudKit

struct FruitModel: Identifiable {
    let id = UUID().uuidString
    let name: String
    let record: CKRecord
}

class CloudKitCRUDBootCampViewModel: ObservableObject {
    @Published var text: String = ""
    @Published var fruits: [FruitModel] = []
    @Published var isUpdatingItem: Bool = false
    var selectedFruit: FruitModel? = nil
    var placeholder: String {
        isUpdatingItem ? "Update \(selectedFruit?.name ?? "Fruit") name with..." : "Add fruit name here..."
    }
    
    init() {
        fetchItems()
    }
    
    func addButtonPressed() {
        guard !text.isEmpty else { return }
        addItem(name: text)
    }
    
    private func addItem(name: String) {
        let newFruit = CKRecord(recordType: "Fruits")
        newFruit["name"] = name
        saveItem(record: newFruit)
    }
    
    private func saveItem(record: CKRecord) {
        CKContainer.default().database(with: .public).save(record) { returnedRecord, returnedError in
            print("Record: \(String(describing: returnedRecord))")
            print("Error: \(String(describing: returnedError))")
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
                guard let self = self else { return }
                self.text = ""
                self.fetchItems()
            }
        }
    }
    
    func fetchItems() {
        let predicate = NSPredicate(value: true)
//        let predicate = NSPredicate(format: "name = %@", argumentArray: ["Watermelon"])
        let query = CKQuery(recordType: "Fruits", predicate: predicate)
        query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        let queryOperation = CKQueryOperation(query: query)
        var returnedItems: [FruitModel] = []
        
        if #available(iOS 15, *) {
            queryOperation.recordMatchedBlock = { (returnedRecordId, returnedResult) in
                switch returnedResult {
                case .success(let record):
                    guard let name = record["name"] as? String else { return }
                    returnedItems.append(FruitModel(name: name, record: record))
                    print(name)
                    break
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        } else {
            queryOperation.recordFetchedBlock = { returnedRecord in
                guard let name = returnedRecord["name"] as? String else { return }
                returnedItems.append(FruitModel(name: name, record: returnedRecord))
            }
        }
        if #available(iOS 15, *) {
            queryOperation.queryResultBlock = { [weak self] returnedResult in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    self.fruits = returnedItems
                }
                print("RETURNED RESULT: \(returnedResult)")
            }
        } else {
            queryOperation.queryCompletionBlock = { [weak self] (returnedCursor, returnedError) in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    self.fruits = returnedItems
                }
                print("RETURNED queryCompletionBlock")
            }
        }
        addOperation(operation: queryOperation)
    }
    
    func updateItem() {
        guard !text.isEmpty && isUpdatingItem, let fruit = selectedFruit else { return }
        let record = fruit.record
        record["name"] = text
        isUpdatingItem = false
        saveItem(record: record)
    }
    
    func deleteItem(indexSet: IndexSet) {
        guard let index = indexSet.first else { return }
        let fruit = fruits[index]
        let record = fruit.record
        
        CKContainer.default().publicCloudDatabase.delete(withRecordID: record.recordID) { [weak self] returnedID, returnedError in
            guard let self = self else { return }
            DispatchQueue.main.async {
                self.fruits.remove(at: index)
            }
        }
    }
    
    func addOperation(operation: CKDatabaseOperation) {
        CKContainer.default().publicCloudDatabase.add(operation)
    }
}
  • CKContainer 데이터베이스 접근 및 비동기적 데이터 처리 함수 CRUD 구현
  • 비동기적 데이터 처리 → 약한 참조 및 메인 스레드 큐 활용
  • 데이터 업데이트 시 Published 데이터이기 때문에 UI 업데이트
  • 클라우드 데이터베이스 접근 이전/이후 데이터 필터링 방법 주의
struct CloudKitCRUDBootCamp: View {
    @StateObject private var viewModel = CloudKitCRUDBootCampViewModel()
    var body: some View {
        NavigationView {
            VStack {
                header
                textField
                button
                List {
                    ForEach(viewModel.fruits) { fruit in
                        Text(fruit.name)
                            .font(.headline)
                            .fontWeight(.semibold)
                            .onTapGesture {
                                viewModel.selectedFruit = fruit
                                viewModel.isUpdatingItem = true
                            }
                    }
                    .onDelete(perform: viewModel.deleteItem)
                }
                .listStyle(.plain)
            }
            .padding()
            .navigationBarHidden(true)
        }
    }
}

extension CloudKitCRUDBootCamp {
    private var header: some View {
        Text("CloudKit CRUD ☁☁☁")
            .font(.headline)
            .underline()
    }
    private var textField: some View {
        TextField(viewModel.placeholder, text: $viewModel.text)
            .frame(height: 55)
            .background(Color.gray.opacity(0.2))
            .cornerRadius(10)
    }
    private var button: some View {
        Button {
            viewModel.isUpdatingItem ? viewModel.updateItem() : viewModel.addButtonPressed()
        } label: {
            Text(viewModel.isUpdatingItem ? "UPDATE" : "ADD")
                .font(.headline)
                .foregroundColor(.black)
                .frame(height: 55)
                .frame(maxWidth: .infinity)
                .background(Color.pink.opacity(0.4))
                .cornerRadius(10)
        }
    }
}
  • 텍스트 필드에 이름을 입력하고 ADD 버튼을 누르면 곧바로 과일 생성
  • 리스트 표시된 과일 탭 제스처 클릭 이후 텍스트 필드에 수정할 이름을 입력하고 UPDATE 버튼을 누르면 과일 정보 갱신
  • 리스트 아이템을 드래그 제스처를 통해 삭제 가능

구현 화면

profile
JUST DO IT

0개의 댓글