VOL. 1 에서는 completionHandler 와 async / await
각각의 방법을 이용해 Data를 Create 하는법에 대해 알아보았다.
이 메서드들의 마지막 부분에서는
Data를 Firestore로부터 Read (이하 Fetch) 하는 메서드가 실행된다.
↪ Create를 함과 동시에 Data를 Fetch하여 landmarks Published-Property에 넣어주고 싶었다.
그럼 오늘은 Data를 Read 하는법과 Trouble-shooting에 대해 기술하려한다.
Cloud Database인 Firestore의 Data를 Read 할 수 있다.
❗ SwiftUI Framework와 MVVM Design Patterm 기반으로 작성되었습니다.
일단 completionHandler를 사용하여 Data를 Fetch 하는 함수다.
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() }
가 담당한다.
여기서
이 문장들을 배경으로 다시 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를 사용해 구현해보았다.
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를 사용했을 때 보다는 비교적 많이 로직의 양이 단축되었다.
처음에 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로 넣어 구현해주면 된다.