Liên kết dữ liệu trên Cloud Firestore bằng Swift Codable

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

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

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

Khi tìm nạp một tài liệu từ Cloud Firestore, ứng dụng của bạn sẽ nhận được một từ điển gồm các cặp khoá/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à các từ điển này sẽ mang lại một số tính linh hoạt tuyệt vời có thể chính là những gì trường hợp sử dụng của bạn cần. Tuy nhiên, phương pháp này không an toàn về kiểu và dễ gây ra lỗi khó theo dõi do lỗi chính tả tên thuộc tính hoặc quên liên kết thuộc tính mới mà nhóm của bạn đã thêm khi phát hành 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 đến các loại Swift. Nhưng xin nhắc lại, hầu hết các phương thức triển khai này đều dựa trên việc chỉ định mối liên kết giữa các tài liệu Cloud Firestore và các loại tương ứng của mô hình dữ liệu của ứng dụng theo cách thủ công.

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

  • Bạn sẽ không phải triển khai mã ánh xạ theo cách thủ công nữa.
  • Bạn có thể dễ dàng xác định cách liên kết các thuộc tính có tên khác nhau.
  • Thư viện này tích hợp sẵn tính năng hỗ trợ cho nhiều loại của Swift.
  • Bạn cũng có thể dễ dàng thêm tính năng hỗ trợ để liên kết các loại tuỳ chỉnh.
  • Hơn hết: đố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.

Ánh xạ dữ liệu

Cloud Firestore lưu trữ dữ liệu trong các tài liệu liên kết khoá vớ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(). Phương thức này sẽ trả về một từ điển ánh xạ tên trường đến 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 vào 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ễ triển khai, nhưng mã này dễ vỡ, khó bảo trì và dễ xảy ra lỗi.

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

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

Và đừng quên rằng chúng ta không tận dụng hệ thống kiểu mạnh của Swift, hệ thống này biết chính xác kiểu 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 vào và ra khỏi một bản trình bày bên ngoài". Trên thực tế, Codable là một kiểu đại diện cho các giao thức Encodable và Decodable. 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ã hoá/giải mã một thực thể của loại này từ định dạng tuần tự, chẳng hạn như JSON.

Một loại đơn giản để lưu trữ dữ liệu về một cuốn sách có thể có dạng như sau:

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 với Codable là tối thiểu. Chúng ta chỉ cần thêm tính năng tuân thủ giao thức; không cần thay đổi gì khác.

Với việc này, chúng ta hiện có thể dễ dàng mã hoá 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)")
}

Quá trình giải mã đối tượng JSON thành một thực thể Book sẽ hoạt động như sau:

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

Liên kết đến và từ các loại đơn giản trong tài liệu Cloud Firestore
bằng cách sử dụng Codable

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

Để liên kết tài liệu Cloud Firestore với các 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. Bạn có thể sử dụng Trình quản lý gói Swift hoặc CocoaPods để thực hiện việc này.
  2. Nhập FirebaseFirestore vào tệp Swift.
  3. Điều chỉnh loại của bạn thành Codable.
  4. (Không bắt buộc, nếu bạn muốn sử dụng loại này 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 liên kết thuộc tính này với mã nhận dạng tài liệu. Chúng tôi sẽ thảo luận chi tiết hơn về vấn đề này bên dưới.
  5. Sử dụng documentReference.data(as: ) để ánh xạ tham chiếu tài liệu đến loại Swift.
  6. Sử dụng documentReference.setData(from: ) để liên kết dữ liệu từ các loại Swift với tài liệu Cloud Firestore.
  7. (Không bắt buộc nhưng bạn nên thực hiện) Triển khai cách xử lý lỗi thích hợp.

Hãy cập nhật loại Book 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ể lập trình, nên chúng ta chỉ cần thêm thuộc tính id và chú thích thuộc tính đó bằng trình bao bọc thuộc tính @DocumentID.

Lấy đoạn mã trước đó để tìm nạp và liên kết một tài liệu, chúng ta có thể thay thế tất cả mã liên kết 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 mã này ngắn gọn hơn bằng cách chỉ định loại tài liệu khi gọi getDocument(as:). Thao tác này sẽ thực hiện việc liên kết cho bạn và trả về một loại Result chứa tài liệu đã liên kết hoặc một lỗi trong trường hợp không thể giải mã:

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

Việc cập nhật một tài liệu hiện có chỉ đơn giản là gọi documentReference.setData(from: ). Bao gồm một số cách xử lý lỗi cơ bản, sau đây là mã để lưu thực thể 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 một tài liệu mới, Cloud Firestore sẽ tự động thực hiện việc chỉ định mã tài liệu mới cho tài liệu đó. Tính năng này thậm chí còn hoạt động khi ứng dụng đang ở chế độ 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 loại dữ liệu đơn giản, Cloud Firestore còn hỗ trợ một số loại dữ liệu khác, một số loại trong đó là các loại 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 một tài liệu.

Kiểu tuỳ chỉnh lồng nhau

Hầu hết các thuộc tính mà chúng ta muốn liên kết trong tài liệu đều là các giá trị đơn giản, chẳng hạn như tên cuố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 ta có thể muốn lưu trữ các URL đến bìa sách ở nhiều độ phân giải.

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ữ một loại tuỳ 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 có hỗ trợ URL – khi lưu trữ một trường chứa URL, trường đó 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?
}

Hãy lưu ý cách chúng ta xác định một 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à không bắt buộc, 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 tò mò lý do 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 điều chỉnh mã để đọc hoặc ghi từ/đến Cloud Firestore: tất cả đều hoạt động với mã chúng ta đã viết trong phần ban đầu.

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. Ví dụ điển hình là các thể loại của cuốn sách: một cuốn sách như The Hitchhiker's Guide to the Galaxy (Hướng dẫn của người đi nhờ xe về dải ngân hà) 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 hước":

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 các giá trị. Tính năng này được hỗ trợ cho mọi loại có thể mã hoá (chẳng hạn như String, Int, v.v.). Phần sau đây cho biết cách thêm một mảng các thể loại vào mô hình Book:

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

Vì phương thức này phù hợp với mọi kiểu có thể lập trình nên chúng ta cũng có thể dùng các kiểu tuỳ chỉnh. Hãy tưởng tượng chúng ta muốn lưu trữ danh sách thẻ cho mỗi cuốn sách. Cùng với tên thẻ, chúng tôi cũng muốn lưu trữ màu của thẻ như sau:

Lưu trữ một mảng các loại tuỳ chỉnh trong tài liệu Firestore

Để lưu trữ 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 để đại diện cho một thẻ và giúp thẻ đó có thể lập trình:

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

Tương tự như vậy, chúng ta có thể lưu trữ một mảng Tags trong các tài liệu Book!

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

Giới thiệu nhanh về việc liên kết mã nhận dạng tài liệu

Trước khi chuyển sang liên kết các loại khác, hãy cùng thảo luận về việc liên kết mã nhận dạng tài liệu.

Chúng ta đã sử dụng trình bao bọc thuộc tính @DocumentID trong một số ví dụ trước đó để liên kết mã nhận dạng 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. Điều này rất quan trọng vì một số lý do:

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

Xin lưu ý rằng một thuộc tính được đánh dấu là @DocumentID sẽ không được mã hoá bằng bộ mã hoá của Cloud Firestore khi ghi lại tài liệu. Nguyên nhân là do mã nhận dạng tài liệu không phải là thuộc tính của chính tài liệu đó. Do đó, bạn có thể nhầm lẫn khi ghi mã này vào tài liệu.

Khi làm việc với các loại 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), bạn 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 tạo thành một tài liệu riêng biệt. Do đó, họ không cần mã số giấy tờ.

Ngày và giờ

Cloud Firestore có một loại dữ liệu tích hợp để xử lý ngày và giờ, đồng thời nhờ tính năng hỗ trợ của Cloud Firestore cho Codable, bạn có thể sử dụng các loại dữ liệu này một cách dễ dàng.

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

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

Loại Swift để liên kết tài liệu này có thể có dạng như sau:

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 có cuộc trò chuyện về @ServerTimestamp. Trình bao bọc thuộc tính này là một yếu tố hỗ trợ mạnh mẽ trong việc xử lý dấu thời gian trong ứng dụng của bạn.

Trong bất kỳ hệ thống phân phối nào, có khả năng là xung nhịp 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ộ hoá. Bạn có thể nghĩ rằng đây không phải là vấn đề lớn, nhưng hãy tưởng tượng những tác động của một đồng hồ chạy hơi không đồng bộ đối với hệ thống giao dịch chứng khoán: ngay cả một sai số mili giây cũng có thể dẫn đến sự khác biệt hàng triệu đô la khi thực hiện một 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 là nil khi bạn lưu trữ thuộc tính đó (ví dụ: sử dụng addDocument()), Cloud Firestore sẽ điền trường bằng dấu thời gian máy chủ hiện tại tại thời điểm ghi thuộc tính đó vào cơ sở dữ liệu. Nếu trường không phải là nil khi bạn gọi addDocument() hoặc updateData(), Cloud Firestore sẽ không thay đổi giá trị thuộc tính. Bằng cách này, bạn có thể dễ dàng triển khai các trường như createdAtlastUpdatedAt.

Điểm địa lý

Vị trí địa lý xuất hiện ở mọi nơi trong các ứng dụng của chúng tôi. Bạn có thể sử dụng nhiều tính năng thú vị bằng cách lưu trữ các tính năng đó. Ví dụ: việc lưu trữ vị trí cho một việc cần làm có thể hữu ích để ứng dụng của bạn có thể nhắc bạn về một việc cần làm khi bạn đến một đích đến.

Cloud Firestore có một kiểu dữ liệu tích hợp, GeoPoint, có thể lưu trữ kinh độ và vĩ độ của bất kỳ vị trí nào. Để liên kết 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ể liên kết 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

Enum có thể là một trong những tính năng ngôn ngữ bị đánh giá thấp nhất trong Swift; và chúng tôi còn có nhiều tính năng khác nữa. Một trường hợp sử dụng phổ biến của enum là mô hình hoá các trạng thái riêng biệt của một đối tượng. Ví dụ: chúng ta 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 ta có thể sử dụng một enum Status:

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

Cloud Firestore không hỗ trợ enum gốc (tức là không thể thực thi tập hợp giá trị), nhưng chúng ta vẫn có thể tận dụng việc có thể nhập enum và chọn một loại có thể lập trình. Trong ví dụ này, chúng tôi đã chọn String, nghĩa là tất cả giá trị enum sẽ được liên kết đến/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ô tuỳ chỉnh, nên chúng ta thậm chí có thể tuỳ chỉnh giá trị nào tham chiếu đến trường hợp enum nào. Ví dụ: nếu quyết định lưu trữ trường hợp Status.inReview dưới dạng "đang xem xét", chúng ta chỉ cần cập nhật enum ở trên như sau:

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

Tuỳ chỉnh mối liên kết

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

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

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

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

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

Để liên kết tài liệu này với một cấu trúc có thuộc tính tên thuộc loại String, chúng ta cần thêm 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, Codable API sẽ sử dụng tên thuộc tính của các loại Swift để xác định tên thuộc tính trên các tài liệu Cloud Firestore mà chúng ta đang cố gắng ánh xạ. Vì vậy, miễn là tên thuộc tính khớp nhau, bạn không cần thêm CodingKeys vào các loại có thể lập trình. Tuy nhiên, sau 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 liên kết.

Trong đoạn mã ở trên, chúng ta đã xác định một thuộc tính id mà chúng ta có thể muốn sử dụng làm giá trị nhận dạng trong thành phần hiển thị List của SwiftUI. Nếu chúng ta không chỉ định trong CodingKeys, thì thuộc tính này sẽ không được liên kết khi tìm nạp dữ liệu và do đó sẽ trở thành nil. Điều này sẽ dẫn đến việc 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 liên kết. Điều này thực sự có thể thuận tiện nếu chúng ta muốn loại trừ một số thuộc tính khỏi việc ánh xạ.

Ví dụ: nếu muốn loại trừ thuộc tính reasonWhyILoveThis khỏi việc được liên kết, tất cả những gì chúng ta cần làm là xoá thuộc tính đó 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 ta có thể muốn ghi một thuộc tính trống trở lại tài liệu Cloud Firestore. Swift có khái niệm về các tuỳ chọn để biểu thị việc không có giá trị và Cloud Firestore cũng hỗ trợ các giá trị null. Tuy nhiên, hành vi mặc định để mã hoá các tuỳ chọn có giá trị nil là chỉ bỏ qua các tuỳ chọn đó. @ExplicitNull cho phép chúng ta kiểm soát một số cách xử lý các tuỳ chọn Swift khi mã hoá: bằng cách gắn cờ một thuộc tính không bắt buộc là @ExplicitNull, chúng ta có thể yêu cầu Cloud Firestore ghi thuộc tính này vào tài liệu có giá trị rỗng nếu tài liệu đó chứa giá trị nil.

Sử dụng bộ mã hoá và bộ giải mã tuỳ chỉnh để ánh xạ màu

Chủ đề cuối cùng trong phần trình bày về việc liên kết dữ liệu bằng Codable, hãy cùng giới thiệu về trình mã hoá và giải mã tuỳ chỉnh. Phần này không đề cập đến loại dữ liệu Cloud Firestore gốc, nhưng các bộ mã hoá và giải mã tuỳ 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 để liên kết màu sắc" 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 việc liên kết giữa Swift và JSON. Có rất nhiều giải pháp, nhưng hầu hết đều tập trung vào JSON và gần như tất cả đề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ó vẻ như phải 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 ký hiệu màu hex cụ thể hơn của CSS) — chúng dễ sử dụng (về cơ bản chỉ là một chuỗi) và thậm chí còn hỗ trợ độ trong suốt!

Để có thể liên kết Color Swift với giá trị hex của nó, chúng ta cần tạo một tiện ích Swift 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ã String thành Color tương đương mà không cần 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 chuyển đổi trước!

Nhờ đó, chúng ta có thể cập nhật mã để liên kết thẻ, giúp dễ dàng xử lý trực tiếp màu thẻ thay vì phải liên kết 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 thiểu việc xử lý lỗi, nhưng trong ứng dụng chính thức, bạn cần đảm bảo xử lý mọi lỗi một cách linh hoạt.

Dưới đâ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 bản cập nhật đang hoạt động

Đoạn mã trước minh hoạ 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ũng hỗ trợ phân phối nội dung cập nhật cho ứng dụng của bạn khi nội dung cập nhật xảy ra, bằng cách sử dụng trình nghe tổng quan nhanh: chúng ta có thể đăng ký trình nghe tổng quan nhanh trên một tập hợp (hoặc truy vấn) và Cloud Firestore sẽ gọi trình nghe của chúng ta bất cứ khi nào có nội dung cập nhật.

Dưới đây là một đoạn mã cho thấy cách đăng ký trình nghe tổng quan nhanh, ánh xạ dữ liệu bằng cách sử dụng Codable và xử lý mọi lỗi có thể xảy ra. Phần này cũng cho biết 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, bạn không cần phải tự cập nhật آرایه cục bộ chứa các tài liệu đã liên kết, vì mã trong trình nghe tổng quan nhanh sẽ xử lý việc này.

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 đều 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.

Tiếp tục và sử dụng Codable!

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

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 đã dần tăng độ phức tạp của mô hình dữ liệu, trong khi có thể dựa vào phương thức triển khai của Firebase và Codable để thực hiện việc liên kết.

Để biết thêm thông tin chi tiết về Codable, bạn nên tham khảo 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 một hướng dẫn toàn diện về cách liên kết 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 để liên kết các loại của mình. Hãy sử dụng nút Gửi phản hồi ở bên dưới để cho chúng tôi biết chiến lược bạn sử dụng để liên kết các loại dữ liệu Cloud Firestore khác hoặc biểu thị 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.