使用 Swift Codable 對應 Cloud Firestore 資料

Swift 4 中推出的 Swift 可編碼 API 可讓我們運用編譯器的強大功能,更輕鬆地將序列化格式的資料對應至 Swift 類型。

您可能會使用 Codable 將網路 API 中的資料對應至應用程式資料模型 (反之亦然),但 Codable 的彈性遠遠不只如此。

在本指南中,我們將探討如何使用 Codable 將資料從 Cloud Firestore 對應至 Swift 類型,反之亦然。

Cloud Firestore 擷取文件時,應用程式會收到鍵/值組合的字典 (如果您使用傳回多份文件的作業之一,則會收到字典陣列)。

您現在可以繼續在 Swift 中直接使用字典,而且字典提供的彈性可能正是您用途所需。不過,這個方法並不安全,很容易因為打錯屬性名稱而造成難以追蹤的錯誤,或團隊在上週發布某項令人期待的新功能時,忘記對應新屬性。

過去,許多開發人員為了解決這些缺點,會實作簡單的對應層,讓他們能夠將字典對應至 Swift 類型。不過,這些實作項目大多是根據手動指定 Cloud Firestore 文件與應用程式資料模型對應類型之間的對應關係。

Cloud Firestore 支援 Swift 的 Codable API,因此這項作業變得更加簡單:

  • 您不再需要手動導入任何對應程式碼。
  • 您可輕鬆定義如何為不同名稱的屬性對應。
  • 內建支援許多 Swift 的型別。
  • 而且,您可以輕鬆新增對應自訂類型的支援。
  • 最棒的是,對於簡單的資料模型,您完全不需要編寫任何對應程式碼。

對應資料

Cloud Firestore 會將資料儲存在文件中,並將鍵對應至值。如要從個別文件擷取資料,我們可以呼叫 DocumentSnapshot.data(),該函式會傳回字典,將欄位名稱對應至 Anyfunc data() -> [String : Any]?

也就是說,我們可以使用 Swift 的子序號語法存取各個個別欄位。

import FirebaseFirestore

#warning("DO NOT MAP YOUR DOCUMENTS MANUALLY. USE CODABLE INSTEAD.")
func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        let id = document.documentID
        let data = document.data()
        let title = data?["title"] as? String ?? ""
        let numberOfPages = data?["numberOfPages"] as? Int ?? 0
        let author = data?["author"] as? String ?? ""
        self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author)
      }
    }
  }
}

雖然這麼做看似簡單易懂,但這段程式碼不穩定、難以維護,而且容易出錯。

如您所見,我們會假設文件欄位的資料類型。不一定正確。

提醒您,由於沒有結構定義,因此您可以輕鬆將新文件新增至集合,並為欄位選擇不同的類型。您可能會不小心為 numberOfPages 欄位選擇字串,導致難以找出對應問題。此外,每次新增欄位時,您都必須更新對應程式碼,這相當麻煩。

別忘了,我們並未善用 Swift 的強型別系統,該系統可精確判斷 Book 的每個屬性正確類型。

什麼是 Codable?

根據 Apple 的說明文件,Codable 是「可自行轉換為外部表示法的類型」。事實上,Codable 是 Encodable 和 Decodable 通訊協定的類型別名。透過遵守 Swift 類型與這個通訊協定,編譯器將合成相關程式碼,用來對這種序列化格式 (例如 JSON) 的例項進行編碼/解碼。

用來儲存書籍資料的簡單型別可能如下所示:

struct Book: Codable {
  var title: String
  var numberOfPages: Int
  var author: String
}

如您所見,將類型符合 Codable 的侵入性最小。我們只需要將相容性加入通訊協定,不需要進行其他變更。

完成這項設定後,我們就能輕鬆將書籍編碼為 JSON 物件:

do {
  let book = Book(title: "The Hitchhiker's Guide to the Galaxy",
                  numberOfPages: 816,
                  author: "Douglas Adams")
  let encoder = JSONEncoder()
  let data = try encoder.encode(book)
} 
catch {
  print("Error when trying to encode book: \(error)")
}

將 JSON 物件解碼為 Book 例項的運作方式如下:

let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)

使用 Codable 在 Cloud Firestore 文件
中對簡單類型進行對應

Cloud Firestore 支援多種資料類型,包括簡單的字串和巢狀地圖。其中大多數都直接對應至 Swift 內建類型。在深入探討較複雜的資料類型之前,我們先來看看如何對應一些簡單的資料類型。

如要將 Cloud Firestore 文件對應至 Swift 類型,請按照下列步驟操作:

  1. 請確認您已將 FirebaseFirestore 架構新增至專案。您可以使用 Swift Package Manager 或 CocoaPods 來執行此操作。
  2. FirebaseFirestore 匯入 Swift 檔案。
  3. 請將你的類型轉換為 Codable
  4. (選用,如果您想在 List 檢視畫面中使用該類型) 在類型中新增 id 屬性,並使用 @DocumentID 告知 Cloud Firestore 將這個屬性對應至文件 ID。我們會在下文中進一步說明這項功能。
  5. 使用 documentReference.data(as: ) 將文件參照對應至 Swift 類型。
  6. 使用 documentReference.setData(from: ) 將 Swift 類型的資料對應至 Cloud Firestore 文件。
  7. (選用,但強烈建議) 實作適當的錯誤處理機制。

讓我們據此更新 Book 類型:

struct Book: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
}

由於此類型已可編碼,我們只需新增 id 屬性,並使用 @DocumentID 屬性包裝函式加註。

使用先前的程式碼片段擷取及對應文件,我們可以用單一行程式碼取代所有手動對應程式碼:

func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        do {
          self.book = try document.data(as: Book.self)
        }
        catch {
          print(error)
        }
      }
    }
  }
}

您也可以在呼叫 getDocument(as:) 時指定文件類型,以更簡潔的方式寫入。系統會為您執行對應作業,並傳回含有對應文件的 Result 類型,或在解碼失敗時傳回錯誤:

private func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)
  
  docRef.getDocument(as: Book.self) { result in
    switch result {
    case .success(let book):
      // A Book value was successfully initialized from the DocumentSnapshot.
      self.book = book
      self.errorMessage = nil
    case .failure(let error):
      // A Book value could not be initialized from the DocumentSnapshot.
      self.errorMessage = "Error decoding document: \(error.localizedDescription)"
    }
  }
}

更新現有文件就像呼叫 documentReference.setData(from: ) 一樣簡單。包括一些基本錯誤處理,以下是儲存 Book 例項的程式碼:

func updateBook(book: Book) {
  if let id = book.id {
    let docRef = db.collection("books").document(id)
    do {
      try docRef.setData(from: book)
    }
    catch {
      print(error)
    }
  }
}

新增文件時,Cloud Firestore 會自動為文件指派新的文件 ID。即使應用程式目前處於離線狀態,也能使用這項功能。

func addBook(book: Book) {
  let collectionRef = db.collection("books")
  do {
    let newDocReference = try collectionRef.addDocument(from: self.book)
    print("Book stored with new document reference: \(newDocReference)")
  }
  catch {
    print(error)
  }
}

除了對應簡單的資料類型,Cloud Firestore 也支援多種其他資料類型,其中部分是結構化類型,可用於在文件中建立巢狀物件。

巢狀自訂類型

我們在文件中要對應的大部分屬性都是簡單的值,例如書籍名稱或作者名稱。但如果需要儲存更複雜的物件,該怎麼辦呢?舉例來說,我們可能會想儲存書籍封面的網址,並提供不同解析度的網址。

如要在 Cloud Firestore 中執行此操作,最簡單的方法就是使用地圖:

在 Firestore 文件中儲存巢狀自訂類型

編寫對應的 Swift 結構時,我們可以利用 Cloud Firestore 支援網址的特性。儲存包含網址的欄位時,系統會將它轉換為字串,反之亦然:

struct CoverImages: Codable {
  var small: URL
  var medium: URL
  var large: URL
}

struct BookWithCoverImages: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var cover: CoverImages?
}

請注意,我們如何為 Cloud Firestore 文件中的封面地圖定義結構體 CoverImages。將 BookWithCoverImages 上的封面屬性標示為選用,我們就能處理部分文件可能不含封面屬性的情況。

如果您好奇為何沒有用於擷取或更新資料的程式碼片段,那麼您會很高興知道,您不需要調整用於讀取或寫入 Cloud Firestore 的程式碼:所有這些程式碼都會與我們在初始部分編寫的程式碼搭配運作。

陣列

有時候,我們會將一組值儲存在文件中。最好將書籍類型當做一個好範例:《The Hitchhiker's Guide to the Galaxy》等書籍可能分為多個類別,在本例中為「科幻」和「喜劇」:

在 Firestore 文件中儲存陣列

Cloud Firestore 中,我們可以使用值陣列模擬這項操作。這項功能支援任何可編碼類型 (例如 StringInt 等)。以下說明如何在 Book 模型中新增陣列類型:

public struct BookWithGenre: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var genres: [String]
}

由於這適用於任何可編碼類型,因此我們也可以使用自訂類型。假設我們想為每本書籍儲存標記清單。除了標記名稱之外,我們也想儲存標記的顏色,如下所示:

將自訂類型的陣列儲存在 Firestore 文件

如要以這種方式儲存標記,我們只需實作 Tag 結構體來代表標記,並讓它可編碼:

struct Tag: Codable, Hashable {
  var title: String
  var color: String
}

就像這樣,我們可在 Book 文件中儲存 Tags 陣列!

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

關於對應文件 ID 的簡短說明

在繼續探索更多類型的對應項目之前,我們先來談談如何對應文件 ID。

我們在先前的部分範例中使用 @DocumentID 屬性包裝函式,將 Cloud Firestore 文件的文件 ID 對應至 Swift 類型的 id 屬性。這麼做十分重要,原因有幾項:

  • 我們可以藉此得知使用者在本機進行變更時,要更新哪份文件。
  • SwiftUI 的 List 要求其元素為 Identifiable,以免元素在插入時跳躍。

值得一提的是,標示為 @DocumentID 的屬性在寫回文件時,不會由 Cloud Firestore 的編碼器編碼。這是因為文件 ID 並非文件本身的屬性,因此將其寫入文件會造成錯誤。

使用巢狀類型 (例如本指南前面範例中的 Book 標記陣列) 時,不需要新增 @DocumentID 屬性:巢狀屬性是 Cloud Firestore 文件的一部分,不構成個別文件。因此不需要文件 ID。

日期和時間

Cloud Firestore 內建的資料類型可用於處理日期和時間,而且由於 Cloud Firestore 支援 Codable,因此使用起來相當簡單。

讓我們看看這份文件,它代表所有程式語言的母親,Ada,於 1843 年發明:

在 Firestore 文件中儲存日期

用於對應此文件的 Swift 類型可能如下所示:

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
}

我們無法在未討論 @ServerTimestamp 的情況下,離開這個關於日期和時間的部分。這個屬性包裝函式在處理應用程式中的時間戳記時,可發揮強大的作用。

在任何分散式系統中,個別系統的時鐘可能不會隨時完全同步。您可能會認為這不是什麼大問題,但請想像一下,如果股票交易系統的時間稍微不同步,可能會導致執行交易時產生數百萬美元的差異。

Cloud Firestore 會以下列方式處理標示為 @ServerTimestamp 的屬性:如果屬性在儲存時為 nil (例如使用 addDocument()),Cloud Firestore 會在將屬性寫入資料庫時,使用目前的伺服器時間戳記填入欄位。如果在呼叫 addDocument()updateData() 時,欄位並非 nilCloud Firestore 就會保留屬性值。這樣一來,就能輕鬆實作 createdAtlastUpdatedAt 等欄位。

地理座標

地理位置廣泛,我們的應用程式無所不在,儲存這些資料後,您就能使用許多令人興奮的功能。舉例來說,儲存工作地點可能很有用,這樣應用程式就能在您抵達目的地時提醒您工作事項。

Cloud Firestore 內建名為 GeoPoint 的資料類型,可儲存任何位置的經緯度。如要將位置對應至 Cloud Firestore 文件,我們可以使用 GeoPoint 類型:

struct Office: Codable {
  @DocumentID var id: String?
  var name: String
  var location: GeoPoint
}

Swift 中的對應類型為 CLLocationCoordinate2D,我們可以使用以下運算將這兩種類型對應:

CLLocationCoordinate2D(latitude: office.location.latitude,
                      longitude: office.location.longitude)

如要進一步瞭解如何依實體位置查詢文件,請參閱這份解決方案指南

列舉

列舉可能是 Swift 中最不受重視的語言功能之一,但其實它比你想像中更強大。列舉的常見用途是模擬某些項目的離散狀態。舉例來說,我們可能會編寫一個應用程式,用來管理文章。如要追蹤文章狀態,我們可能會使用列舉 Status

enum Status: String, Codable {
  case draft
  case inReview
  case approved
  case published
}

Cloud Firestore 原生不支援列舉 (也就是說,它無法強制執行一組值),但我們仍然可以利用列舉可輸入的類型,並選擇可組合的類型。在本例中,我們選擇了 String,這表示當所有列舉值儲存在 Cloud Firestore 文件中時,會對應至/從字串。

由於 Swift 支援自訂原始值,因此我們甚至可以自訂哪些值參照哪些枚舉值。舉例來說,假設我們決定將 Status.inReview 案例儲存為「in review」,即可更新上述列舉,如下所示:

enum Status: String, Codable {
  case draft
  case inReview = "in review"
  case approved
  case published
}

自訂對應項目

有時,我們要對應的 Cloud Firestore 文件屬性名稱,與 Swift 中資料模型屬性名稱不符。舉例來說,我們的同事可能會是 Python 開發人員,並決定為所有屬性名稱選擇 snake_case。

別擔心,Codable 會幫你解決這個問題!

對於這類情況,我們可以使用 CodingKeys。這是我們可以新增至可編碼結構體的列舉,用來指定如何對應特定屬性。

請參考下列文件:

使用蛇形命名法的 Firestore 文件

如要將這份文件對應至名稱屬性類型為 String 的結構,必須在 ProgrammingLanguage 結構中加入 CodingKeys 列舉,並在文件中指定屬性名稱:

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

根據預設,Codable API 會使用 Swift 類型的屬性名稱,判斷我們嘗試對應的 Cloud Firestore 文件中的屬性名稱。因此,只要屬性名稱相符,就無須將 CodingKeys 新增至可編碼類型。不過,一旦我們為特定類型使用 CodingKeys,就必須新增所有要對應的資源名稱。

在上方程式碼片段中,我們定義了 id 屬性,可能會用於在 SwiftUI List 檢視畫面中做為 ID。如未在 CodingKeys 中指定,擷取時就不會對應,因此會變成 nil。這會導致 List 檢視畫面填入第一份文件。

在對應的 CodingKeys 列舉中,如果屬性未列為案例,系統會在對應過程中忽略該屬性。事實上,如要明確排除某些屬性,這個方法會很方便。

舉例來說,如果我們想從對應作業中排除 reasonWhyILoveThis 屬性,只需從 CodingKeys 列舉中移除即可:

struct ProgrammingLanguage: Identifiable, Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  var reasonWhyILoveThis: String = ""
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

有時我們可能會想將空屬性寫回 Cloud Firestore 文件。Swift 具有選用概念來表示缺少值,而 Cloud Firestore 也支援 null 值。不過,如果要編碼具有 nil 值的選用項目,預設行為是略過這些項目。@ExplicitNull 可讓您控管 Swift 選用項目編碼時的處理方式:將選用屬性標記為 @ExplicitNull,即可指示 Cloud Firestore 在文件包含 nil 值時,以空值在文件中寫入該屬性。

使用自訂編碼器和解碼器對應顏色

最後一個主題是 Codable 對應資料,接下來介紹自訂編碼器和解碼器。本節不會介紹原生 Cloud Firestore 資料類型,但自訂編碼器和解碼器在 Cloud Firestore 應用程式中非常實用。

「如何對應顏色」是開發人員最常提出的問題之一,不僅是 Cloud Firestore,也包括 Swift 和 JSON 之間的對應。市面上有許多解決方案,但大多數解決方案都著重在 JSON,而幾乎所有解決方案都會將顏色對應至由 RGB 元件組成的巢狀字典。

似乎應該有更簡單的解決方案。為什麼不使用網頁顏色 (或更具體地說,CSS 十六進位顏色符號)?這些顏色很容易使用 (基本上就是字串),甚至支援透明度!

如要將 Swift Color 對應至其十六進位值,我們需要建立 Swift 擴充功能,將 Codable 新增至 Color

extension Color {

 init(hex: String) {
    let rgba = hex.toRGBA()

    self.init(.sRGB,
              red: Double(rgba.r),
              green: Double(rgba.g),
              blue: Double(rgba.b),
              opacity: Double(rgba.alpha))
    }

    //... (code for translating between hex and RGBA omitted for brevity)

}

extension Color: Codable {
  
  public init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let hex = try container.decode(String.self)

    self.init(hex: hex)
  }
  
  public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(toHex)
  }

}

只要使用 decoder.singleValueContainer(),就可以將 String 解碼為對等的 Color,而不必建立 RGBA 元件的巢狀結構。此外,您可以在應用程式的網頁式 UI 中使用這些值,無須先進行轉換!

有了這個功能,我們就能更新標記對應的程式碼,直接處理標記顏色,而不需要在應用程式的 UI 程式碼中手動對應:

struct Tag: Codable, Hashable {
  var title: String
  var color: Color
}

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

處理錯誤

在上述程式碼片段中,我們刻意將錯誤處理作業降到最低,但在實際工作環境中的應用程式中,您必須確保能妥善處理任何錯誤。

以下程式碼片段說明如何處理可能遇到的任何錯誤情況:

class MappingSimpleTypesViewModel: ObservableObject {
  @Published var book: Book = .empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  
  func fetchAndMap() {
    fetchBook(documentId: "hitchhiker")
  }
  
  func fetchAndMapNonExisting() {
    fetchBook(documentId: "does-not-exist")
  }
  
  func fetchAndTryMappingInvalidData() {
    fetchBook(documentId: "invalid-data")
  }
  
  private func fetchBook(documentId: String) {
    let docRef = db.collection("books").document(documentId)
    
    docRef.getDocument(as: Book.self) { result in
      switch result {
      case .success(let book):
        // A Book value was successfully initialized from the DocumentSnapshot.
        self.book = book
        self.errorMessage = nil
      case .failure(let error):
        // A Book value could not be initialized from the DocumentSnapshot.
        switch error {
        case DecodingError.typeMismatch(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.valueNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.keyNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.dataCorrupted(let key):
          self.errorMessage = "\(error.localizedDescription): \(key)"
        default:
          self.errorMessage = "Error decoding document: \(error.localizedDescription)"
        }
      }
    }
  }
}

處理即時更新中的錯誤

先前的程式碼片段說明如何處理擷取單一文件的錯誤。除了擷取資料一次,Cloud Firestore 也支援使用所謂的快照事件監聽器,在發生時向應用程式傳送更新:我們可以在集合 (或查詢) 上註冊快照事件監聽器,Cloud Firestore 會在發生更新時呼叫事件監聽器。

以下程式碼片段說明如何註冊快照事件監聽器、使用 Codable 對應資料,以及處理可能發生的任何錯誤。以及如何將新文件新增至集合如您所見,我們不需要自行更新用於儲存對應文件的本機陣列,因為快照事件監聽器中的程式碼會負責這項作業。

class MappingColorsViewModel: ObservableObject {
  @Published var colorEntries = [ColorEntry]()
  @Published var newColor = ColorEntry.empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  private var listenerRegistration: ListenerRegistration?
  
  public func unsubscribe() {
    if listenerRegistration != nil {
      listenerRegistration?.remove()
      listenerRegistration = nil
    }
  }
  
  func subscribe() {
    if listenerRegistration == nil {
      listenerRegistration = db.collection("colors")
        .addSnapshotListener { [weak self] (querySnapshot, error) in
          guard let documents = querySnapshot?.documents else {
            self?.errorMessage = "No documents in 'colors' collection"
            return
          }
          
          self?.colorEntries = documents.compactMap { queryDocumentSnapshot in
            let result = Result { try queryDocumentSnapshot.data(as: ColorEntry.self) }
            
            switch result {
            case .success(let colorEntry):
              if let colorEntry = colorEntry {
                // A ColorEntry value was successfully initialized from the DocumentSnapshot.
                self?.errorMessage = nil
                return colorEntry
              }
              else {
                // A nil value was successfully initialized from the DocumentSnapshot,
                // or the DocumentSnapshot was nil.
                self?.errorMessage = "Document doesn't exist."
                return nil
              }
            case .failure(let error):
              // A ColorEntry value could not be initialized from the DocumentSnapshot.
              switch error {
              case DecodingError.typeMismatch(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.valueNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.keyNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.dataCorrupted(let key):
                self?.errorMessage = "\(error.localizedDescription): \(key)"
              default:
                self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
              }
              return nil
            }
          }
        }
    }
  }
  
  func addColorEntry() {
    let collectionRef = db.collection("colors")
    do {
      let newDocReference = try collectionRef.addDocument(from: newColor)
      print("ColorEntry stored with new document reference: \(newDocReference)")
    }
    catch {
      print(error)
    }
  }
}

本文中使用的程式碼片段全都屬於範例應用程式,您可以從這個 GitHub 存放區下載。

快去使用 Codable 吧!

Swift 的 Codable API 提供強大且彈性的功能,可將序列化格式的資料對應至應用程式資料模型,反之亦然。在本指南中,您已瞭解如何在使用 Cloud Firestore 做為資料儲存庫的應用程式中輕鬆使用這項功能。

我們從一個含有簡單資料類型的簡單範例開始,逐步增加資料模型的複雜度,同時也能依賴 Codable 和 Firebase 的實作項目,為我們執行對應作業。

如要進一步瞭解 Codable,建議您參考下列資源:

雖然我們盡力編寫完整的指南,說明如何對應 Cloud Firestore 文件,但這並非詳盡無遺,您可能會使用其他策略來對應類型。請使用下方的「傳送意見」按鈕,告訴我們您在對應其他類型的 Cloud Firestore 資料或在 Swift 中表示資料時,採用哪些策略。

您其實沒有理由不使用 Cloud Firestore 的 Codable 支援功能。