Ánh xạ dữ liệu Cloud Firestore với Swift Codable

API có thể mã hóa của Swift, được giới thiệu trong Swift 4, cho phép chúng tôi tận dụng sức mạnh của trình biên dịch để giúp ánh xạ dữ liệu từ các định dạng được tuần tự hóa sang các loại Swift dễ dàng hơn.

Bạn có thể đã sử dụng Codable để ánh xạ dữ liệu từ API web sang mô hình dữ liệu của ứng dụng (và ngược lại), nhưng nó linh hoạt hơn thế nhiều.

Trong hướng dẫn này, chúng ta sẽ xem cách Codable có thể được sử dụng để ánh xạ dữ liệu từ Cloud Firestore sang các loại Swift và ngược lại.

Khi tìm nạp tài liệu từ Cloud Firestore, ứng dụng của bạn sẽ nhận được từ điển gồm các cặp khóa/giá trị (hoặc một mảng từ điển, nếu bạn sử dụng một trong các thao tác trả về nhiều tài liệu).

Giờ đây, bạn chắc chắn có thể tiếp tục sử dụng trực tiếp từ điển trong Swift và chúng mang lại sự linh hoạt tuyệt vời có thể chính xác là những gì trường hợp sử dụng của bạn yêu cầu. Tuy nhiên, cách tiếp cận này không an toàn về mặt hình thức và rất dễ gây ra các lỗi khó theo dõi do viết sai chính tả tên thuộc tính hoặc quên ánh xạ thuộc tính mới mà nhóm của bạn đã thêm khi họ tung ra tính năng mới thú vị đó vào tuần trước.

Trước đây, nhiều nhà phát triển đã khắc phục những thiếu sót này bằng cách triển khai một lớp ánh xạ đơn giản cho phép họ ánh xạ từ điển sang các loại Swift. Nhưng một lần nữa, hầu hết các cách triển khai này đều dựa trên việc chỉ định thủ công ánh xạ giữa các tài liệu Cloud Firestore và các loại mô hình dữ liệu ứng dụng tương ứng của bạn.

Với sự hỗ trợ của Cloud Firestore dành cho Codable API của Swift, việc này trở nên dễ dàng hơn rất nhiều:

  • Bạn sẽ không còn phải triển khai thủ công bất kỳ mã ánh xạ nào nữa.
  • Thật dễ dàng để xác định cách ánh xạ các thuộc tính với các tên khác nhau.
  • Nó có hỗ trợ tích hợp cho nhiều loại Swift.
  • Và thật dễ dàng để thêm hỗ trợ cho việc ánh xạ các loại tùy chỉnh.
  • Điều tuyệt vời nhất là: đối với các mô hình dữ liệu đơn giản, bạn sẽ không phải viết bất kỳ mã ánh xạ nào cả.

Dữ liệu bản đồ

Cloud Firestore lưu trữ dữ liệu trong tài liệu ánh xạ khóa tới giá trị. Để tìm nạp dữ liệu từ một tài liệu riêng lẻ, chúng ta có thể gọi DocumentSnapshot.data() , nó trả về một từ điển ánh xạ tên trường thành Any : func data() -> [String : Any]? .

Điều này có nghĩa là chúng ta có thể sử dụng cú pháp chỉ số dưới của Swift để truy cập từng trường riêng lẻ.

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)
      }
    }
  }
}

Mặc dù có vẻ đơn giản và dễ thực hiện nhưng mã này rất dễ vỡ, khó bảo trì và dễ bị lỗi.

Như bạn có thể thấy, chúng tôi đang đưa ra các giả định về loại dữ liệu của các trường tài liệu. Những điều này có thể đúng hoặc có thể không.

Hãy nhớ rằng, vì không có lược đồ nên bạn có thể dễ dàng thêm tài liệu mới vào bộ sưu tập và chọn loại khác cho một trường. Bạn có thể vô tình chọn chuỗi cho trường numberOfPages , điều này sẽ dẫn đến sự cố ánh xạ khó tìm. Ngoài ra, bạn sẽ phải cập nhật mã ánh xạ của mình bất cứ khi nào trường mới được thêm vào, điều này khá cồng kềnh.

Và đừng quên rằng chúng ta không tận dụng được hệ thống kiểu mạnh của Swift, hệ thống này biết chính xác loại chính xác cho từng thuộc tính của Book .

Codable là gì?

Theo tài liệu của Apple, Codable là "một loại có thể tự chuyển đổi thành và ngoài biểu diễn bên ngoài". Trên thực tế, Codable là bí danh loại cho các giao thức có thể mã hóa và giải mã được. Bằng cách tuân thủ loại Swift theo giao thức này, trình biên dịch sẽ tổng hợp mã cần thiết để mã hóa/giải mã một phiên bản của loại này từ định dạng được tuần tự hóa, chẳng hạn như JSON.

Một kiểu đơn giản để lưu trữ dữ liệu về một cuốn sách có thể trông như thế này:

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

Như bạn có thể thấy, việc tuân thủ loại này thành Codable có mức độ xâm lấn tối thiểu. Chúng tôi chỉ phải thêm sự tuân thủ vào giao thức; không có thay đổi nào khác được yêu cầu.

Với điều này, giờ đây chúng ta có thể dễ dàng mã hóa một cuốn sách thành đối tượng 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)")
}

Việc giải mã một đối tượng JSON thành một phiên bản Book hoạt động như sau:

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

Ánh xạ tới và từ các loại đơn giản trong tài liệu Cloud Firestore
sử dụng mã hóa

Cloud Firestore hỗ trợ nhiều loại dữ liệu, từ các chuỗi đơn giản đến các bản đồ lồng nhau. Hầu hết trong số này tương ứng trực tiếp với các kiểu có sẵn của Swift. Trước tiên, chúng ta hãy xem việc ánh xạ một số loại dữ liệu đơn giản trước khi chúng ta đi sâu vào các loại dữ liệu phức tạp hơn.

Để ánh xạ tài liệu Cloud Firestore sang loại Swift, hãy làm theo các bước sau:

  1. Đảm bảo bạn đã thêm khung FirebaseFirestore vào dự án của mình. Bạn có thể sử dụng Trình quản lý gói Swift hoặc CocoaPods để làm như vậy.
  2. Nhập FirebaseFirestore vào tệp Swift của bạn.
  3. Tuân thủ loại của bạn thành Codable .
  4. (Tùy chọn, nếu bạn muốn sử dụng loại trong chế độ xem List ) Thêm thuộc tính id vào loại của bạn và sử dụng @DocumentID để yêu cầu Cloud Firestore ánh xạ thuộc tính này tới ID tài liệu. Chúng ta sẽ thảo luận điều này chi tiết hơn dưới đây.
  5. Sử dụng documentReference.data(as: ) để ánh xạ tham chiếu tài liệu tới loại Swift.
  6. Sử dụng documentReference.setData(from: ) để ánh xạ dữ liệu từ các loại Swift sang tài liệu Cloud Firestore.
  7. (Tùy chọn, nhưng rất khuyến khích) Thực hiện xử lý lỗi thích hợp.

Hãy cập nhật loại Book của chúng tôi cho phù hợp:

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

Vì loại này đã có thể mã hóa được nên chúng tôi chỉ phải thêm thuộc tính id và chú thích nó bằng trình bao bọc thuộc tính @DocumentID .

Lấy đoạn mã trước đó để tìm nạp và ánh xạ tài liệu, chúng ta có thể thay thế tất cả mã ánh xạ thủ công bằng một dòng duy nhất:

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)
        }
      }
    }
  }
}

Bạn có thể viết phần này chính xác hơn bằng cách chỉ định loại tài liệu khi gọi getDocument(as:) . Điều này sẽ thực hiện ánh xạ cho bạn và trả về loại Result chứa tài liệu được ánh xạ hoặc lỗi trong trường hợp giải mã không thành công:

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)"
    }
  }
}

Cập nhật một tài liệu hiện có cũng đơn giản như gọi documentReference.setData(from: ) . Bao gồm một số cách xử lý lỗi cơ bản, đây là mã để lưu phiên bản 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)
    }
  }
}

Khi thêm tài liệu mới, Cloud Firestore sẽ tự động đảm nhiệm việc gán ID tài liệu mới cho tài liệu. Điều này thậm chí hoạt động khi ứng dụng hiện đang ngoại tuyến.

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)
  }
}

Ngoài việc ánh xạ các kiểu dữ liệu đơn giản, Cloud Firestore còn hỗ trợ một số kiểu dữ liệu khác, một số trong số đó là kiểu có cấu trúc mà bạn có thể sử dụng để tạo các đối tượng lồng nhau bên trong tài liệu.

Các loại tùy chỉnh lồng nhau

Hầu hết các thuộc tính mà chúng tôi muốn ánh xạ trong tài liệu của mình đều là các giá trị đơn giản, chẳng hạn như tên sách hoặc tên tác giả. Nhưng còn những trường hợp chúng ta cần lưu trữ một đối tượng phức tạp hơn thì sao? Ví dụ: chúng tôi có thể muốn lưu trữ URL tới bìa sách ở các độ phân giải khác nhau.

Cách dễ nhất để thực hiện việc này trong Cloud Firestore là sử dụng bản đồ:

Lưu trữ loại tùy chỉnh lồng nhau trong tài liệu Firestore

Khi viết cấu trúc Swift tương ứng, chúng ta có thể tận dụng thực tế là Cloud Firestore hỗ trợ URL - khi lưu trữ một trường có chứa URL, nó sẽ được chuyển đổi thành chuỗi và ngược lại:

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?
}

Lưu ý cách chúng tôi xác định cấu trúc, CoverImages , cho bản đồ bìa trong tài liệu Cloud Firestore. Bằng cách đánh dấu thuộc tính bìa trên BookWithCoverImages là tùy chọn, chúng tôi có thể xử lý thực tế là một số tài liệu có thể không chứa thuộc tính bìa.

Nếu bạn tò mò tại sao không có đoạn mã để tìm nạp hoặc cập nhật dữ liệu, bạn sẽ rất vui khi biết rằng không cần phải điều chỉnh mã để đọc hoặc ghi từ/đến Cloud Firestore: tất cả điều này đều hoạt động với mã của chúng tôi đã viết ở phần đầu tiên.

Mảng

Đôi khi, chúng ta muốn lưu trữ một tập hợp các giá trị trong một tài liệu. Thể loại của một cuốn sách là một ví dụ điển hình: một cuốn sách như Hướng dẫn về thiên hà của người quá giang có thể thuộc một số danh mục — trong trường hợp này là "Khoa học viễn tưởng" và "Hài kịch":

Lưu trữ một mảng trong tài liệu Firestore

Trong Cloud Firestore, chúng ta có thể lập mô hình này bằng cách sử dụng một mảng giá trị. Điều này được hỗ trợ cho mọi loại có thể mã hóa (chẳng hạn như String , Int , v.v.). Phần sau đây cho thấy cách thêm một loạt thể loại vào mô hình Book của chúng tôi:

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

Vì cách này hoạt động với mọi loại có thể mã hóa nên chúng tôi cũng có thể sử dụng các loại tùy chỉnh. Hãy tưởng tượng chúng ta muốn lưu trữ một danh sách các thẻ cho mỗi cuốn sách. Cùng với tên của thẻ, chúng tôi cũng muốn lưu trữ màu của thẻ như thế này:

Lưu trữ một loạt các loại tùy chỉnh trong tài liệu Firestore

Để lưu trữ các thẻ theo cách này, tất cả những gì chúng ta cần làm là triển khai cấu trúc Tag để thể hiện một thẻ và làm cho nó có thể mã hóa được:

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

Và cứ như thế, chúng ta có thể lưu trữ một mảng Tags trong tài liệu Book của mình!

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

Đôi điều ngắn gọn về ánh xạ ID tài liệu

Trước khi chúng ta chuyển sang ánh xạ nhiều loại hơn, hãy nói một chút về việc ánh xạ ID tài liệu.

Chúng tôi đã sử dụng trình bao bọc thuộc tính @DocumentID trong một số ví dụ trước để ánh xạ ID tài liệu của tài liệu Cloud Firestore với thuộc tính id của các loại Swift của chúng tôi. Điều này quan trọng vì một số lý do:

  • Nó giúp chúng tôi biết tài liệu nào cần cập nhật trong trường hợp người dùng thực hiện các thay đổi cục bộ.
  • List của SwiftUI yêu cầu các phần tử của nó phải có Identifiable để ngăn các phần tử nhảy lung tung khi chúng được chèn vào.

Cần chỉ ra rằng thuộc tính được đánh dấu là @DocumentID sẽ không được mã hóa bởi bộ mã hóa của Cloud Firestore khi ghi lại tài liệu. Điều này là do ID tài liệu không phải là thuộc tính của chính tài liệu — vì vậy việc ghi ID tài liệu vào tài liệu sẽ là một sai lầm.

Khi làm việc với các kiểu lồng nhau (chẳng hạn như mảng thẻ trên Book trong ví dụ trước đó trong hướng dẫn này), không bắt buộc phải thêm thuộc tính @DocumentID : các thuộc tính lồng nhau là một phần của tài liệu Cloud Firestore và không cấu thành một tài liệu riêng biệt. Do đó, họ không cần ID tài liệu.

Ngày và giờ

Cloud Firestore có kiểu dữ liệu tích hợp sẵn để xử lý ngày và giờ và nhờ sự hỗ trợ của Cloud Firestore dành cho Codable nên việc sử dụng chúng rất đơn giản.

Chúng ta hãy xem tài liệu đại diện cho mẹ của tất cả các ngôn ngữ lập trình, Ada, được phát minh vào năm 1843:

Lưu trữ ngày tháng trong tài liệu Firestore

Loại Swift để ánh xạ tài liệu này có thể trông như thế này:

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

Chúng ta không thể rời khỏi phần này về ngày và giờ mà không trò chuyện về @ServerTimestamp . Trình bao bọc thuộc tính này là một công cụ mạnh mẽ khi xử lý dấu thời gian trong ứng dụng của bạn.

Trong bất kỳ hệ thống phân tán nào, rất có thể đồng hồ trên các hệ thống riêng lẻ không phải lúc nào cũng hoàn toàn đồng bộ. Bạn có thể nghĩ đây không phải là vấn đề lớn, nhưng hãy tưởng tượng tác động của việc đồng hồ hơi không đồng bộ đối với hệ thống giao dịch chứng khoán: ngay cả sai lệch một phần nghìn giây cũng có thể dẫn đến chênh lệch hàng triệu đô la khi thực hiện giao dịch.

Cloud Firestore xử lý các thuộc tính được đánh dấu bằng @ServerTimestamp như sau: nếu thuộc tính này bằng nil khi bạn lưu trữ nó (ví dụ: sử dụng addDocument() ), Cloud Firestore sẽ điền vào trường có dấu thời gian của máy chủ hiện tại tại thời điểm ghi nó vào cơ sở dữ liệu . Nếu trường nil khi bạn gọi addDocument() hoặc updateData() , Cloud Firestore sẽ giữ nguyên giá trị thuộc tính. Bằng cách này, thật dễ dàng để triển khai các trường như createdAtlastUpdatedAt .

Địa điểm

Định vị địa lý có mặt khắp nơi trong ứng dụng của chúng tôi. Nhiều tính năng thú vị có thể thực hiện được bằng cách lưu trữ chúng. Ví dụ: có thể hữu ích khi lưu trữ vị trí cho một nhiệm vụ để ứng dụng của bạn có thể nhắc nhở bạn về một nhiệm vụ khi bạn đến đích.

Cloud Firestore có kiểu dữ liệu tích hợp là GeoPoint , có thể lưu trữ kinh độ và vĩ độ của bất kỳ vị trí nào. Để ánh xạ các vị trí từ/đến tài liệu Cloud Firestore, chúng ta có thể sử dụng loại GeoPoint :

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

Loại tương ứng trong Swift là CLLocationCoordinate2D và chúng ta có thể ánh xạ giữa hai loại đó bằng thao tác sau:

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

Để tìm hiểu thêm về cách truy vấn tài liệu theo vị trí thực tế, hãy xem hướng dẫn giải pháp này .

Enum

Enums có lẽ là một trong những tính năng ngôn ngữ được đánh giá thấp nhất trong Swift; có nhiều thứ về họ hơn những gì bạn thấy. Trường hợp sử dụng phổ biến của enum là mô hình hóa các trạng thái riêng biệt của một thứ gì đó. Ví dụ: chúng tôi có thể đang viết một ứng dụng để quản lý bài viết. Để theo dõi trạng thái của một bài viết, chúng tôi có thể muốn sử dụng enum Status :

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

Cloud Firestore vốn không hỗ trợ enum (tức là nó không thể thực thi tập hợp các giá trị), nhưng chúng ta vẫn có thể tận dụng thực tế là enum có thể được nhập và chọn loại có thể mã hóa. Trong ví dụ này, chúng tôi đã chọn String , có nghĩa là tất cả các giá trị enum sẽ được ánh xạ tới/từ chuỗi khi được lưu trữ trong tài liệu Cloud Firestore.

Và, vì Swift hỗ trợ các giá trị thô tùy chỉnh, nên chúng tôi thậm chí có thể tùy chỉnh giá trị nào tham chiếu đến trường hợp enum nào. Vì vậy, ví dụ: nếu chúng tôi quyết định lưu trữ trường hợp Status.inReview là "đang xem xét", chúng tôi có thể cập nhật enum ở trên như sau:

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

Tùy chỉnh bản đồ

Đôi khi, tên thuộc tính của tài liệu Cloud Firestore mà chúng tôi muốn ánh xạ không khớp với tên thuộc tính trong mô hình dữ liệu của chúng tôi trong Swift. Ví dụ: một trong những đồng nghiệp của chúng tôi có thể là nhà phát triển Python và đã quyết định chọn snake_case cho tất cả các tên thuộc tính của họ.

Đừng lo lắng: Codable đã hỗ trợ chúng tôi!

Đối với những trường hợp như thế này, chúng ta có thể sử dụng CodingKeys . Đây là một enum mà chúng ta có thể thêm vào cấu trúc có thể mã hóa để chỉ định cách ánh xạ các thuộc tính nhất định.

Hãy xem xét tài liệu này:

Tài liệu Firestore có tên thuộc tính snake_cased

Để ánh xạ tài liệu này tới một cấu trúc có thuộc tính name thuộc loại String , chúng ta cần thêm một enum CodingKeys vào cấu trúc ProgrammingLanguage và chỉ định tên của thuộc tính trong tài liệu:

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
  }
}

Theo mặc định, API có thể mã hóa sẽ sử dụng tên thuộc tính của các loại Swift của chúng tôi để xác định tên thuộc tính trên tài liệu Cloud Firestore mà chúng tôi đang cố gắng ánh xạ. Vì vậy, miễn là tên thuộc tính khớp nhau thì không cần thêm CodingKeys vào các loại có thể mã hóa của chúng tôi. Tuy nhiên, khi sử dụng CodingKeys cho một loại cụ thể, chúng ta cần thêm tất cả tên thuộc tính mà chúng ta muốn ánh xạ.

Trong đoạn mã ở trên, chúng tôi đã xác định thuộc tính id mà chúng tôi có thể muốn sử dụng làm mã định danh trong chế độ xem List SwiftUI. Nếu chúng ta không chỉ định nó trong CodingKeys thì nó sẽ không được ánh xạ khi tìm nạp dữ liệu và do đó trở thành nil . Điều này sẽ dẫn đến chế độ xem List được lấp đầy bằng tài liệu đầu tiên.

Bất kỳ thuộc tính nào không được liệt kê dưới dạng trường hợp trên enum CodingKeys tương ứng sẽ bị bỏ qua trong quá trình ánh xạ. Điều này thực sự có thể thuận tiện nếu chúng ta đặc biệt muốn loại trừ một số thuộc tính khỏi ánh xạ.

Vì vậy, ví dụ: nếu chúng ta muốn loại trừ thuộc tính reasonWhyILoveThis khỏi ánh xạ, tất cả những gì chúng ta cần làm là xóa nó khỏi enum 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
  }
}

Đôi khi, chúng tôi có thể muốn ghi lại một thuộc tính trống vào tài liệu Cloud Firestore. Swift có khái niệm về các tùy chọn để biểu thị sự vắng mặt của một giá trị và Cloud Firestore cũng hỗ trợ các giá trị null . Tuy nhiên, hành vi mặc định để mã hóa các tùy chọn có giá trị nil là chỉ bỏ qua chúng. @ExplicitNull cung cấp cho chúng tôi một số quyền kiểm soát về cách xử lý các tùy chọn Swift khi mã hóa chúng: bằng cách gắn cờ một thuộc tính tùy chọn là @ExplicitNull , chúng tôi có thể yêu cầu Cloud Firestore ghi thuộc tính này vào tài liệu với giá trị null nếu nó chứa giá trị nil .

Sử dụng bộ mã hóa và bộ giải mã tùy chỉnh để ánh xạ màu

Là chủ đề cuối cùng trong phạm vi đề cập đến dữ liệu ánh xạ với Codable của chúng tôi, hãy giới thiệu bộ mã hóa và giải mã tùy chỉnh. Phần này không bao gồm kiểu dữ liệu Cloud Firestore gốc nhưng bộ mã hóa và giải mã tùy chỉnh rất hữu ích trong các ứng dụng Cloud Firestore của bạn.

"Làm cách nào tôi có thể ánh xạ màu" là một trong những câu hỏi thường gặp nhất của nhà phát triển, không chỉ đối với Cloud Firestore mà còn đối với ánh xạ giữa Swift và JSON. Có rất nhiều giải pháp hiện có, nhưng hầu hết chúng đều tập trung vào JSON và hầu hết chúng đều ánh xạ màu sắc dưới dạng một từ điển lồng nhau bao gồm các thành phần RGB của nó.

Có vẻ như nên có một giải pháp tốt hơn, đơn giản hơn. Tại sao chúng ta không sử dụng màu web (hoặc cụ thể hơn là ký hiệu màu hex CSS) — chúng dễ sử dụng (về cơ bản chỉ là một chuỗi) và thậm chí chúng còn hỗ trợ tính minh bạch!

Để có thể ánh xạ Swift Color tới giá trị hex của nó, chúng ta cần tạo tiện ích mở rộng Swift có thêm Codable vào 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)
  }

}

Bằng cách sử dụng decoder.singleValueContainer() , chúng ta có thể giải mã một String thành Color tương đương của nó mà không cần phải lồng các thành phần RGBA. Ngoài ra, bạn có thể sử dụng các giá trị này trong giao diện người dùng web của ứng dụng mà không cần phải chuyển đổi chúng trước!

Với điều này, chúng tôi có thể cập nhật mã để ánh xạ thẻ, giúp xử lý trực tiếp màu thẻ dễ dàng hơn thay vì phải ánh xạ chúng theo cách thủ công trong mã giao diện người dùng của ứng dụng:

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]
}

Xử lý lỗi

Trong các đoạn mã trên, chúng tôi cố tình giữ mức xử lý lỗi ở mức tối thiểu, nhưng trong ứng dụng sản xuất, bạn sẽ muốn đảm bảo xử lý mọi lỗi một cách khéo léo.

Đây là đoạn mã cho biết cách sử dụng để xử lý mọi tình huống lỗi mà bạn có thể gặp phải:

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)"
        }
      }
    }
  }
}

Xử lý lỗi trong cập nhật trực tiếp

Đoạn mã trước trình bày cách xử lý lỗi khi tìm nạp một tài liệu. Ngoài việc tìm nạp dữ liệu một lần, Cloud Firestore còn hỗ trợ cung cấp các bản cập nhật cho ứng dụng của bạn khi chúng xảy ra, bằng cách sử dụng cái gọi là trình nghe ảnh chụp nhanh: chúng tôi có thể đăng ký trình nghe ảnh chụp nhanh trên một bộ sưu tập (hoặc truy vấn) và Cloud Firestore sẽ gọi cho trình nghe của chúng tôi bất cứ khi nào ở đó là một bản cập nhật

Đây là đoạn mã cho biết cách đăng ký trình nghe ảnh chụp nhanh, ánh xạ dữ liệu bằng Codable và xử lý mọi lỗi có thể xảy ra. Nó cũng chỉ ra cách thêm một tài liệu mới vào bộ sưu tập. Như bạn sẽ thấy, không cần phải tự cập nhật mảng cục bộ chứa các tài liệu được ánh xạ, vì việc này đã được mã trong trình nghe ảnh chụp nhanh xử lý.

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)
    }
  }
}

Tất cả các đoạn mã được sử dụng trong bài đăng này là một phần của ứng dụng mẫu mà bạn có thể tải xuống từ kho lưu trữ GitHub này .

Hãy tiếp tục và sử dụng Codable!

API có thể mã hóa của Swift cung cấp một cách mạnh mẽ và linh hoạt để ánh xạ dữ liệu từ các định dạng được tuần tự hóa đến và từ mô hình dữ liệu ứng dụng của bạn. Trong hướng dẫn này, bạn đã thấy cách sử dụng dễ dàng như thế nào trong các ứng dụng sử dụng Cloud Firestore làm kho dữ liệu của chúng.

Bắt đầu từ một ví dụ cơ bản với các kiểu dữ liệu đơn giản, chúng tôi đã tăng dần độ phức tạp của mô hình dữ liệu, đồng thời có thể dựa vào cách triển khai của Codable và Firebase để thực hiện ánh xạ cho chúng tôi.

Để biết thêm chi tiết về Codable, tôi khuyên dùng các tài nguyên sau:

Mặc dù chúng tôi đã cố gắng hết sức để biên soạn hướng dẫn toàn diện về cách ánh xạ các tài liệu Cloud Firestore nhưng hướng dẫn này chưa đầy đủ và bạn có thể đang sử dụng các chiến lược khác để ánh xạ các loại của mình. Sử dụng nút Gửi phản hồi bên dưới, hãy cho chúng tôi biết bạn sử dụng chiến lược nào để ánh xạ các loại dữ liệu Cloud Firestore khác hoặc thể hiện dữ liệu trong Swift.

Thực sự không có lý do gì để không sử dụng tính năng hỗ trợ Codable của Cloud Firestore.