[SwiftUI] - Cloud Firestore에 Data를 Create, Read (VOL. 2)

Ben·2023년 5월 25일
0

iOS

목록 보기
4/11
post-thumbnail

VOL. 1 에서는 completionHandler 와 async / await
각각의 방법을 이용해 Data를 Create 하는법에 대해 알아보았다.

  • uploadDataToFirestore()
  • uploadDataToFirestoreWithAsyncAwait()

이 메서드들의 마지막 부분에서는
Data를 Firestore로부터 Read (이하 Fetch) 하는 메서드가 실행된다.
↪ Create를 함과 동시에 Data를 Fetch하여 landmarks Published-Property에 넣어주고 싶었다.

그럼 오늘은 Data를 Read 하는법과 Trouble-shooting에 대해 기술하려한다.

Overview

Cloud Database인 Firestore의 Data를 Read 할 수 있다.

❗ SwiftUI Framework와 MVVM Design Patterm 기반으로 작성되었습니다.

Implementation

일단 completionHandler를 사용하여 Data를 Fetch 하는 함수다.

  • (Method) fetchDataWithCompletionHandlerFromFirestore()
func fetchDataWithCompletionHandlerFromFirestore(completionHandler: @escaping (Result<[Landmark], FirestoreError>) -> Void) throws -> Void {
        
	collectionRef.order(by: "id").getDocuments { querySnapshot, error in
            
		if let error: Error = error {
                
			fatalError("Error getting documents: \(error.localizedDescription)")
		} else if (querySnapshot?.documents.isEmpty == true) {
                
            //  Firestore에 Data가 없다면...
            print("Documents does not exist.")
                
            completionHandler(.failure(FirestoreError.documentsNotFound))
                
            do {
                    
                //  uploadDataToFirestore() 실행
                try self.uploadDataToFirestore()
            } catch {
                    
                print("uploadDataToFirestore() error: \(error.localizedDescription)")
                    
                completionHandler(.failure(FirestoreError.uploadDataFailed))
            }
                
            return
        } else if (querySnapshot?.documents.isEmpty == false) {
                
            //  Firestore에 Data가 있다면 Fetch Data!
            print("Documents does exist.")
                
            self.collectionRef.order(by: "id").getDocuments { querySnapshot, error in
                    
                if let error: Error = error {
                        
                    fatalError("Error getting documents: \(error.localizedDescription)")
                } else {
                        
                    self.landmarks.removeAll()
                        
                    if let snapshot: QuerySnapshot = querySnapshot {
                            
                        for document in snapshot.documents {
                                
                            let documentData: [String: Any] = document.data()
                                
                            let name: String = documentData["name"] as? String ?? ""
                            let category: String = documentData["category"] as? String ?? ""
                            let city: String = documentData["city"] as? String ?? ""
                            let state: String = documentData["state"] as? String ?? ""
                            let id: Int = documentData["id"] as? Int ?? 0
                            let isFeatured: Bool = documentData["isFeatured"] as? Bool ?? false
                            let isFavorite: Bool = documentData["isFavorite"] as? Bool ?? false
                            let park: String = documentData["park"] as? String ?? ""
                            let description: String = documentData["description"] as? String ?? ""
                            let coordinates: [String: Double] = documentData["coordinates"] as? [String: Double] ?? ["coordinates": 0.0]
                                
                            let landmark: Landmark = Landmark(name: name,
                                                              category: category,
                                                              city: city,
                                                              state: state,
                                                              id: id,
                                                              isFeatured: isFeatured,
                                                              isFavorite: isFavorite,
                                                              park: park,
                                                              description: description,
                                                              imageName: name,
                                                              coordinates: ["latitude": coordinates["latitude"] ?? 0.0,
                                                                            "longitude": coordinates["longitude"] ?? 0.0])
                                
                            self.landmarks.append(landmark)
                                
                            DispatchQueue.main.async {
                                    
                                    completionHandler(.success(self.landmarks))
                            }
                        }
                    }
                }
            }
        }
            
        return
    }
}

요약하자면 Firestore의 documents를 가져오는데, 만약 documents가 비어있으면

uploadDataToFirestore() 메서드로 Data Create를 해주고 document가 비어있지 않다면 Data Fetch를 해서 landmarks 배열에 landmark 객체를 추가해준다.

그러면서 실제 Main-Thread에서는 UI를 그리는 작업이 이루어지기 때문에
View에서 메서드를 실행시 데이터를 비동기처리로 가져와야한다.
이는 DispatchQueue.main.async { completionHandler() } 가 담당한다.

여기서

  • Data가 없으면 Create 해준다
  • Create를 함과 동시에 Data를 Fetch

이 문장들을 배경으로 다시 uploadDataToFirestore() 메서드를 재구성하면

private func uploadDataToFirestore() throws -> Void {
        
        decodedLandmarks.forEach { landmark in
            
            // MARK: - setData(_: [String: Any], completion: ((Error?) -> Void)? = nil)
            //	Create 생략
        }
        
        do {
            
            try self.fetchDataWithCompletionHandlerFromFirestore { result in
                
                switch result {
                case .success(let landmark):
                    
                    self.landmarks = landmark
                    break;
                    
                case .failure(let error):
                    
                    switch error {
                    case .documentsNotFound:
                        break;
                        
                    case .uploadDataFailed:
                        break;
                        
                    case .fetchDataFailed:
                        break;
                    }
                }
            }
        } catch {
            
            print("fetchDataWithCompletionHandlerFromFirestore() error: \(error.localizedDescription)")
        }
        
        return
    }

fetchDataWithCompletionHandlerFromFirestore()의 Flow가 상당히 복잡해지면서 'deeply-nested closures'가 요구된다.

Firestore에 Data가 없다는 가정하에 fetchDataWithCompletionHandlerFromFirestore() 를 실행시키면

임의로 넣은 view들이 Live Preview에 잘 적용되어 나타났고
console에도 landmark들이 저장되어 fetch 까지 되는 것을 확인할 수 있다.


이제, fetchDataWithCompletionHandlerFromFirestore()를 async/await를 사용해 구현해보았다.

  • (Method) fetchDataFromFirestoreWithAsyncAwait()
func fetchDataFromFirestoreWithAsyncAwait() async throws -> [Landmark] {
        
        do {
            
            if (try await collectionRef.getDocuments().isEmpty == true) {
                
                try await uploadDataToFirestoreWithAsyncAwait()
            } else {
                
                self.landmarks.removeAll()
                
                let querySnapshot: QuerySnapshot = try await collectionRef.order(by: "id").getDocuments()
                
                querySnapshot.documents.forEach { queryDocumentSnapshot in
                    
                    let documentData: [String: Any] = queryDocumentSnapshot.data()
                    
                    let name: String = documentData["name"] as? String ?? ""
                    let category: String = documentData["category"] as? String ?? ""
                    let city: String = documentData["city"] as? String ?? ""
                    let state: String = documentData["state"] as? String ?? ""
                    let id: Int = documentData["id"] as? Int ?? 0
                    let isFeatured: Bool = documentData["isFeatured"] as? Bool ?? false
                    let isFavorite: Bool = documentData["isFavorite"] as? Bool ?? false
                    let park: String = documentData["park"] as? String ?? ""
                    let description: String = documentData["description"] as? String ?? ""
                    let coordinates: [String: Double] = documentData["coordinates"] as? [String: Double] ?? ["coordinates": 0.0]
                    
                    let landmark: Landmark = Landmark(name: name,
                                                      category: category,
                                                      city: city,
                                                      state: state,
                                                      id: id,
                                                      isFeatured: isFeatured,
                                                      isFavorite: isFavorite,
                                                      park: park,
                                                      description: description,
                                                      imageName: name,
                                                      coordinates: ["latitude": coordinates["latitude"] ?? 0.0,
                                                                    "longitude": coordinates["longitude"] ?? 0.0])
                    
                    self.landmarks.append(landmark)
                }
            }
        } catch {
            
            print(error.localizedDescription)
        }
        
        return self.landmarks
    }

completionHandler를 사용했을 때 보다는 비교적 많이 로직의 양이 단축되었다.

Trouble-shooting 💥

처음에 fetchDataWithCompletionHandlerFromFirestore()의 제일 안쪽
새로운 landmark 상수를 만들어 주는 과정에서

let name: String = documentData["name"] as? String ?? ""
let category: String = documentData["category"] as? String ?? ""
let city: String = documentData["city"] as? String ?? ""
let state: String = documentData["state"] as? String ?? ""
let id: Int = documentData["id"] as? Int ?? 0
let isFeatured: Bool = documentData["isFeatured"] as? Bool ?? false
let isFavorite: Bool = documentData["isFavorite"] as? Bool ?? false
let park: String = documentData["park"] as? String ?? ""
let description: String = documentData["description"] as? String ?? ""
let coordinates: [String: Double] = documentData["coordinates"] as? [String: Double] ?? ["coordinates": 0.0]

이 상수들로 구성된 인스턴스 객체를 생성을 할 수가 없었다.

원인은 간단했다.

struct Landmark: Hashable, Codable, Identifiable {
    
    // MARK: - Stored-Props
    var name: String
    var category: Category
    var city: String
    var state: String
    var id: Int
    var isFeatured: Bool
    var isFavorite: Bool
    var park: String
    var description: String
    
    private var imageName: String
    private var coordinates: Coordinates
    
    // MARK: - Computed-Props
    enum Category: String, CaseIterable, Codable {
        
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
        case none = "NONE"
    }
    
    // MARK: - Inner-Structure
    struct Coordinates: Hashable, Codable {
        
        // MARK: - Stored-Props
        var latitude: Double
        var longitude: Double
    }
    
    // MARK: - Enum Category
    enum Category: String, CaseIterable, Codable {
        
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
        case none = "NONE"
    }
}

만들려고 하는 인스턴스의 값에는 Landmark 구조체의 모든 Property가 존재하지 않고, 일부만을 이용해서 인스턴스를 생성할 것이기에

extension Landmark {
    
    init(name: String, category: String, city: String, state: String, id: Int, isFeatured: Bool, isFavorite: Bool,
         park: String, description: String, imageName: String, coordinates: [String: Double]) {
        
        self.name = name
        
        switch category {
        case "Lakes":
            self.category = .lakes
            break;
            
        case "Rivers":
            self.category = .rivers
            break;
            
        case "Mountains":
            self.category = .mountains
            break;
            
        default:
            self.category = .none
            break;
        }
        
        self.city = city
        self.state = state
        self.id = id
        self.isFeatured = isFeatured
        self.isFavorite = isFavorite
        self.park = park
        self.description = description
        
        if (name == "Lake Umbagog") {
            
            self.imageName = name.lowercased().replacingOccurrences(of: "lake ", with: "")
        } else {
            
            self.imageName = name.lowercased().replacingOccurrences(of: " ", with: "", options: .regularExpression).components(separatedBy: ".").joined()
        }
        
        self.coordinates = Coordinates(latitude: coordinates["latitude"] ?? 0.0,
                                       longitude: coordinates["longitude"] ?? 0.0)
    }
}

구조체를 extension시켜 인스턴스 생성에 필요한 Args를 init()의 Params로 넣어 구현해주면 된다.

profile
 iOS Developer

0개의 댓글