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

Codable API 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 để ánh xạ dữ liệu từ các định dạng được chuyển đổi tuần tự đến Swift dễ dàng hơn loại.

Có thể bạn đã sử dụng tính năng Codable để ánh xạ dữ liệu từ API web đến dữ liệu của ứng dụng (và ngược lại), nhưng 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 để lập bản đồ dữ liệu từ Các loại từ Cloud Firestore sang 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 của 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 trực tiếp sử dụng từ điển trong Swift và chúng cung cấp một số tính linh hoạt tuyệt vời có thể giống như trường hợp sử dụng của bạn. Tuy nhiên, phương pháp này không an toàn về kiểu và rất dễ giới thiệu các lỗi khó theo dõi do tên thuộc tính lỗi chính tả hoặc quên sử dụng bản đồ thuộc tính mới mà nhóm của bạn đã thêm khi họ gửi tính năng mới thú vị đó 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 chúng ánh xạ từ điển tới Loại Swift. Nhưng xin nhắc lại, hầu hết các phương pháp triển khai này đều được áp dụng theo cách thủ công chỉ định mối liên kết giữa các tài liệu trên Cloud Firestore và các loại mô hình dữ liệu tương ứng của ứng dụng.

Với sự hỗ trợ của Cloud Firestore dành cho Codable API của Swift, công nghệ này sẽ dễ dàng hơn:

  • Bạn sẽ không còn phải triển khai bất kỳ mã ánh xạ nào 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.
  • API này có tính năng hỗ trợ tích hợp cho nhiều kiểu Swift.
  • Bạn cũng có thể dễ dàng thêm tính năng hỗ trợ liên kết các kiểu 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 mã ánh xạ.

Dữ liệu liên kết

Cloud Firestore lưu trữ dữ liệu trong các tài liệu liên kết khoá với các giá trị. Để tìm nạp từ một tài liệu riêng lẻ, chúng ta có thể gọi DocumentSnapshot.data() sẽ trả về một từ điển ánh xạ các tên trường với một 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ó duy trì và dễ bị lỗi.

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

Lưu ý rằng vì không có giản đồ nên bạn có thể dễ dàng thêm tài liệu mới đối với bộ sưu tập và chọn một 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 việc trong một vấn đề kh�� tìm thấy lập bản đồ. Ngoài ra, bạn sẽ phải cập nhật lập bản đồ của mình mỗi khi thêm một trường mới, việc này khá rườm rà.

Và hãy nhớ rằng chúng ta không tận dụng kiểu mạnh mẽ của Swift hệ thống biết chính xác loại chính xác cho mỗi thuộc tính của Book.

Codable là gì?

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

Kiểu đơ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 theo Codable là rất ít xâm phạm. Chỉ chúng tôi phải thêm tính tuân thủ vào giao thức; không cần thực hiện thay đổi nào khác.

Với vị trí này, giờ đây chúng tôi có thể dễ dàng mã hoá 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 thực thể 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)

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

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

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

  1. Hãy đả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 để làm đi��u đó.
  2. Nhập FirebaseFirestore vào tệp Swift.
  3. Chuyển đổi loại thành Codable.
  4. (Không bắt buộc, nếu bạn muốn sử dụng loại trong khung hiển thị List) Thêm id vào loại của bạn, rồi sử dụng @DocumentID để yêu cầu Cloud Firestore cho liên kết mã 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 Swift loại.
  6. Sử dụng documentReference.setData(from: ) để ánh xạ dữ liệu từ các kiểu Swift đến một Tài liệu trên Cloud Firestore.
  7. (Không bắt buộc, nhưng nên dùng) Triển khai cách xử lý lỗi đúng cách.

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ể mã hoá, nên chúng ta chỉ phải thêm thuộc tính id và chú thích 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 mã này ngắn gọn hơn nữa bằng cách chỉ định loại tài liệu khi gọi getDocument(as:). Việc này sẽ thực hiện việc ánh xạ cho bạn và trả về loại Result chứa tài liệu được liên kết hoặc một 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 tài liệu hiện có cũng đơn giản như gọi điện documentReference.setData(from: ). Bao gồm một số cách xử lý lỗi cơ bản, tại đâ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 bạn thêm một tài liệu mới, Cloud Firestore sẽ tự động xử lý gán một mã nhận dạng tài liệu mới cho tài liệu. Tính năng này thậm chí còn hiệu quả 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 liên kết các loại dữ liệu đơn giản, Cloud Firestore còn hỗ trợ các loại dữ liệu khác, một số trong số đó 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 tôi muốn ánh xạ trong tài liệu của mình 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 không? Ví dụ: chúng tôi có thể muốn lưu trữ URL vào 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 tuỳ chỉnh được lồng trong tài liệu trên Firestore

Khi viết cấu trúc Swift tương ứng, chúng ta có thể sử dụng thực tế là Cloud Firestore hỗ trợ URL. Khi lưu trữ trường có chứa URL, sẽ được chuyển đổi thành một 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 trên Cloud Firestore. Bằng cách đánh dấu tài sản bìa trên BookWithCoverImages là không bắt buộc, chúng ta 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 muốn biết 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ẽ hài lòng khi biết rằng không cần điều chỉnh mã để đọc hoặc ghi từ/vào Cloud Firestore: tất cả đều hoạt động với mã được viết trong phần đầ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. Thể loại của một cuốn sách là ví dụ điển hình: một cuốn sách như The Hitchhiker's Guide to the Galaxy 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":

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

Trong Cloud Firestore, chúng ta có thể lập mô hình dữ liệu này bằng cách sử dụng một mảng giá trị. Đây là được hỗ trợ cho mọi loại có thể lập trình (chẳng hạn như String, Int, v.v.). Nội dung sau đây hướng dẫn 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ì 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. Tưởng tượng chúng tôi muốn lưu trữ danh sách thẻ cho mỗi cuốn sách. Cùng với tên của , 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 về 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à làm cho thẻ đó có thể mã hoá:

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ề liên kết ID tài liệu

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

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 để liên kết mã nhận dạng của các tài liệu trong Cloud Firestore với thuộc tính id các loại Swift của chúng tôi. Điều này quan trọng vì một số lý do:

  • Việc này 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 tạo thay đổi.
  • 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 chúng được chèn.

Xin lưu ý rằng thuộc tính được đánh dấu là @DocumentID sẽ không được được mã hoá bằng bộ mã hoá của Cloud Firestore khi soạn lại tài liệu. Đây là vì 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 đó. Vì vậy, viết thông tin này vào tài liệu sẽ là sai lầm.

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

Ngày và giờ

Cloud Firestore có một loại dữ liệu tích hợp sẵn cho ngày và giờ xử lý, và nhờ sự hỗ trợ của Cloud Firestore dành cho Codable, nên dễ dàng sử dụng chúng.

Hãy cùng 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 tháng trong tài liệu trên 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 trò chuyện giới thiệu về @ServerTimestamp. Trình bao bọc tài sản này là một giải pháp mạnh mẽ để hỗ trợ bạn 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, khả năng cao là đồ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 được đồng bộ hoá. Bạn có thể nghĩ đây không phải là nhưng hãy tưởng tượng hệ quả của việc đồng hồ chạy hơi không đồng bộ đối với hệ thống thương mại chứng khoán: thậm chí chỉ một sai lệch một mili giây cũng có thể dẫn đến sự chênh lệch hàng triệu USD 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 là sau: nếu thuộc tính là nil khi bạn lưu trữ (sử dụng addDocument(), cho ví dụ), Cloud Firestore sẽ điền sẵn máy chủ hiện tại vào trường này tại thời điểm ghi dữ liệu 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ẽ rời giá trị thuộc tính chưa được chạm đến. 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ý có mặt ở khắp nơi trong các ứng dụng của chúng tôi. Nhiều tính năng thú vị sẽ trở nên khả thi bằng cách lưu trữ chúng. Ví dụ: có thể bạn nên lưu trữ thông tin vị trí cho một công việc để ứng dụng có thể nhắc bạn về một nhiệm vụ khi bạn đến một đích đến.

Cloud Firestore có một loại dữ liệu tích hợp sẵn là GeoPoint, có thể lưu trữ kinh độ và vĩ độ của bất kỳ vị trí nào. Để lập bản đồ các vị trí từ/đến Trong 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
}

Kiểu dữ liệu 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 tham khảo hướng dẫn giải pháp này.

Enum

Enum có lẽ là một trong những tính năng ngôn ngữ bị đánh giá thấp nhất trong Swift; có rất nhiều thứ khác ngoài mong đợi. Một trường hợp sử dụng phổ biến của enum là lập mô hình các trạng thái rời rạc của một vật nào đó. Ví dụ: chúng ta có thể 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 một cách tự nhiên (tức là không thể thực thi tập giá trị), nhưng chúng ta vẫn có thể tận dụng thực tế là enum có thể nhập được, rồi chọn một loại có thể lập trình. Trong ví dụ này, chúng ta đã chọn String, nghĩa là tất cả giá trị enum sẽ được ánh xạ đến/từ chuỗi khi được lưu trữ trong một Tài liệu trên Cloud Firestore.

Và vì Swift hỗ trợ các giá trị thô tuỳ chỉnh, nên chúng ta thậm chí còn có thể tuỳ chỉnh những giá trị nào tham chiếu đến trường hợp enum nào. 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 ta chỉ có thể cập nhật enum ở trên thà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 trên Cloud Firestore mà chúng ta muốn ánh xạ không khớp với tên của thuộc tính trong mô hình dữ liệu của chúng ta trong Swift. Ví dụ: một đồng nghiệp của chúng tôi có thể là nhà phát triển Python và quyết định chọn bullet_case cho tất cả tên thuộc tính của chúng.

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

Đối với các 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 một cấu trúc có thể lập trình để chỉ định cách ánh xạ một số thuộc 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 postal_cased

Để ánh xạ 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 một enum CodingKeys vào cấu trúc ProgrammingLanguage rồi chỉ định tên của thuộc tính trong chứng từ:

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 tài liệu Cloud Firestore mà chúng tôi đang thử để lập bản đồ. Vì vậy, miễn là tên thuộc tính trùng khớp, bạn không cần thêm CodingKeys cho các loại có thể lập trình của chúng ta. Tuy nhiên, sau khi sử dụng CodingKeys cho một loại cụ thể, chúng tôi cần thêm tất cả tên cơ sở lưu trú mà chúng tôi muốn ánh xạ.

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

Bất kỳ thuộc tính nào không được liệt kê là 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 tôi đặc biệt muốn loại trừ một số cơ sở lưu trú khỏi việc ánh xạ.

Ví dụ: nếu chúng ta muốn loại trừ thuộc tính reasonWhyILoveThis khỏi đang được ánh xạ, bạn chỉ cần xoá 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 ta có thể muốn viết lại một thuộc tính trống vào Tài liệu trên Cloud Firestore. Swift có khái niệm về thuộc tính không bắt buộc để biểu thị 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 không bắt buộc có giá trị nil là bỏ qua. @ExplicitNull cấp cho chúng ta một số quyền kiểm soát đối với cách Swift các tùy chọn được xử lý khi mã hóa chúng: bằng cách gắn cờ thuộc tính tùy chọn 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 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 bài viết về dữ liệu bản đồ bằng Codable, hãy cùng giới thiệu bộ mã hoá và giải mã tuỳ chỉnh. Phần này không đề cập đến quảng cáo gốc Tuy loại dữ liệu trên Cloud Firestore, 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 để ánh xạ 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ỉ cho Cloud Firestore mà còn để ánh xạ giữa Swift và JSON dưới dạng tốt. Có rất nhiều giải pháp, nhưng hầu hết trong số đó tập trung vào JSON, và hầu hết tất cả đều ánh xạ màu dưới dạng từ điển lồng nhau bao gồm RGB thành phần.

Có vẻ như cần có một giải pháp tốt hơn và đơn giản hơn. Tại sao chúng tôi 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í còn hỗ trợ tính minh bạch!

Để 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 Swift tiện ích bổ sung 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 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 phải chuyển đổi chúng đầu tiên!

Bằng cách này, chúng ta có thể cập nhật mã cho các thẻ ánh xạ, giúp xử lý thay vì phải liên kết 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ố ý duy trì khả năng xử lý lỗi ở mức tối thiểu, nhưng trong ứng dụng chính thức, bạn cần đảm bảo xử lý .

Dưới đây là đoạn mã cho biết cách sử dụng cách 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òn hỗ trợ phân phối bản cập nhật cho ứng dụng ngay khi chúng diễn ra, bằng cách sử dụng tính năng trình nghe: chúng ta có thể đăng ký trình nghe chụp 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 mỗi khi có bản cập nhật.

Đây là một đoạn mã cho biết cách đăng ký trình nghe chụp nhanh, dữ liệu bản đồ bằng cách sử dụng Codable và xử lý mọi lỗi có thể xảy ra. Hướng dẫ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 cập nhật mảng cục bộ lưu giữ các tài liệu đã ánh xạ, vì việc này được xử lý của mã trong trình nghe chụp nhanh.

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 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 qua kho lưu trữ GitHub.

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

Codable API của Swift mang đến một cách thức mạnh mẽ và linh hoạt để ánh xạ dữ liệu từ được chuyển đổi tuần tự đến và đi 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 rất dễ sử dụng trong những ứng dụng 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 dần đã làm tăng độ phức tạp của mô hình dữ liệu, đồng thời vẫn có thể dựa vào Triển khai của Codable và Firebase để giúp chúng tôi 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 lập bản đồ Tài liệu này chưa đầy đủ trong Cloud Firestore và có thể bạn đang dùng để liên kết các loại video của mình. Bằng cách 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 để lập bản đồ các loại Dữ liệu trên Cloud Firestore hoặc biểu thị dữ liệu bằng 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.