API Codable của Swift (ra mắt 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 được chuyển đổi tuần tự sang các loại Swift.
Có thể bạn đã sử dụng Codable để liên kết dữ liệu từ một API web với mô hình dữ liệu của ứng dụng (và ngược lại), nhưng Codable 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 để ánh xạ dữ liệu từ Cloud Firestore sang 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 các từ điển trong Swift và chúng mang lại một số tính 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ề kiểu và bạn có thể dễ dàng gặp phải các lỗi khó theo dõi bằng cách viết sai 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ọ 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 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 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 trong ứng dụng.
Với sự hỗ trợ của Cloud Firestore 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 phải triển khai mã liên kết theo cách thủ công nữa.
- Bạn có thể dễ dàng xác định cách ánh xạ các thuộc tính có tên khác nhau.
- Nó có hỗ trợ sẵn cho nhiều loại của Swift.
- Bạn có thể dễ dàng thêm tính năng hỗ trợ cho việc lập bản đồ các loại tuỳ 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.
Dữ liệu liên kết
Cloud Firestore lưu trữ dữ liệu trong các tài liệu ánh xạ khoá đến 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()
. Hàm này trả về một từ điển ánh xạ tên trường đến 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 lại dễ bị lỗi, khó duy trì và dễ xảy ra lỗi.
Như bạn thấy, chúng tôi đang đưa ra các giả định về kiểu dữ liệu của các trường trong tài liệu. Những thông tin này có thể chính xác hoặc không.
Xin lưu ý rằng vì không có giả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 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 vấn đề liên kết khó tìm. Ngoài ra, bạn sẽ phải cập nhật mã ánh xạ bất cứ khi nào có một trường mới được thêm vào, đ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 phù hợp 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à từ một biểu thức bên ngoài". Trên thực tế, Codable là một bí danh kiểu cho các giao thức Encodable và Decodable. Bằng cách tuân thủ một 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 phiên bản của loại này từ một định dạng được chuyển đổi 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ể trông như sau:
struct Book: Codable {
var title: String
var numberOfPages: Int
var author: String
}
Như bạn có thể thấy, việc điều chỉnh loại cho phù hợp với Codable là ít xâm nhập nhất. Chúng tôi chỉ phải thêm sự tuân thủ vào giao thức; không cần thay đổi gì khác.
Với thông tin này, giờ đây chúng ta 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)")
}
Việc giải mã một đối tượng JSON thành một thực thể Book
sẽ diễn ra như sau:
let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)
Ánh xạ đến và từ các kiểu đơn giản trong tài liệu Cloud Firestore
bằng Codable
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 các loại này đều 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 xét 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.
Để liên kết các tài liệu Cloud Firestore với các loại Swift, hãy làm theo các bước sau:
- Đả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 để thực hiện việc này. - Nhập
FirebaseFirestore
vào tệp Swift của bạn. - Điều chỉnh kiểu của bạn cho phù hợp với
Codable
. - (Không bắt buộc, nếu bạn muốn sử dụng loại này trong khung hiển thị
List
) Thêm một thuộc tínhid
vào loại của bạn và sử dụng@DocumentID
để cho Cloud Firestore biết cách ánh xạ thuộc tính này đến mã nhận dạng tài liệu. Chúng ta sẽ thảo luận chi tiết hơn về vấn đề này ở phần dưới đây. - Sử dụng
documentReference.data(as: )
để liên kết một tài liệu tham chiếu với một loại Swift. - Dùng
documentReference.setData(from: )
để ánh xạ dữ liệu từ các loại Swift sang tài liệu Cloud Firestore. - (Không bắt buộc nhưng bạn nên thực hiện) Triển khai quy trình 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ể mã hoá, nên chúng ta chỉ cần 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ạ một 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 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:)
. Thao tác này sẽ thực hiện việc ánh xạ cho bạn và trả về một loại Result
chứa tài liệu được ánh xạ 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)"
}
}
}
Việc 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ố hoạt động xử lý lỗi cơ bản, sau đây là mã để lưu một 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 đảm nhận việc chỉ định mã tài liệu mới cho tài liệu đó. Tính năng này hoạt động ngay cả khi ứng dụng hiện không kết nối mạng.
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 số đó là các loại có cấu trúc mà bạn có thể dùng để tạo các đối tượng lồng nhau bên trong một tài liệu.
Các loại 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ư tiêu đề của sách hoặc tên của tác giả. Nhưng trong 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ữ 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 một bản đồ:
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 chứa URL, trường đó 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?
}
Lưu ý cách chúng ta xác định một cấu trúc, CoverImages
, cho bản đồ trên trang bìa trong tài liệu Cloud Firestore. Bằng cách đánh dấu thuộc tính cover trên BookWithCoverImages
là không bắt buộc, chúng ta có thể xử lý trường hợp một số tài liệu có thể không chứa thuộc tính cover.
Nếu tò mò về lý do 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ừ/đến Cloud Firestore: tất cả những điều này đều hoạt động với mã 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. 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ư The Hitchhiker's Guide to the Galaxy (Cuốn tự truyện của một người đi nhờ xe xuyên dải ngân hà) có thể thuộc nhiều danh mục – trong trường hợp này là "Khoa học viễn tưởng" và "Hài hước":
Trong Cloud Firestore, chúng ta có thể mô hình hoá việc 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ã hoá (chẳng hạn như String
, Int
, v.v.). Sau đây là cách thêm một mảng thể loại vào mô hình Book
của chúng ta:
public struct BookWithGenre: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
var genres: [String]
}
Vì điều này áp dụng cho mọi loại có thể mã hoá, nên chúng ta cũng có thể dùng các loại tuỳ chỉnh. Hãy tưởng tượng rằ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 của thẻ, chúng ta cũng muốn lưu trữ màu của thẻ, như sau:
Để 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 một cấu trúc Tag
để biểu thị một thẻ và làm cho thẻ đó có thể mã hoá:
struct Tag: Codable, Hashable {
var title: String
var color: String
}
Và như vậy, chúng ta có thể lưu trữ một mảng Tags
trong 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 ánh xạ nhiều loại hơn, hãy dành chút thời gian để nói về việc ánh xạ 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 sau:
- Đ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 các thay đổi cục bộ.
List
của SwiftUI yêu cầu các phần tử phải làIdentifiable
để ngăn các phần tử nhảy xung quanh khi chúng được chèn.
Bạn nên lưu ý rằng một thuộc tính được đánh dấu là @DocumentID
sẽ không được bộ mã hoá của Cloud Firestore mã hoá khi ghi lại tài liệu. Điều này là do mã nhận dạng tài liệu không phải là một thuộc tính của chính tài liệu đó, vì vậy, việc ghi mã nhận dạng vào tài liệu sẽ là một sai lầm.
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ã tài liệu.
Ngày và giờ
Cloud Firestore có một kiểu dữ liệu tích hợp để xử lý ngày và giờ, đồng thời nhờ khả năng hỗ trợ Codable của Cloud Firestore, bạn có thể sử dụng các kiểu dữ liệu này một cách đơn giản.
Hãy xem tài liệu này, đây là ngôn ngữ lập trình mẹ của mọi ngôn ngữ lập trình, Ada, được phát minh vào năm 1843:
Một 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ể bỏ qua phần này về ngày và giờ mà không thảo luậ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 mọi hệ thống phân tán, có thể đồng hồ trên các hệ thống riêng lẻ không hoàn toàn đồng bộ mọi lúc. 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 chiếc đồng hồ chạy lệch nhịp một chút đối với hệ thống giao dịch chứng khoán: ngay cả độ 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 là nil
khi bạn lưu trữ (ví dụ: bằng cách sử dụng addDocument()
), Cloud Firestore sẽ điền trường bằng dấu thời gian hiện tại của máy chủ tại thời điểm ghi 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ẽ giữ nguyên 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ư createdAt
và lastUpdatedAt
.
Điểm địa lý
Thông tin về vị trí địa lý xuất hiện ở khắp mọi nơi trong các ứng dụng của chúng ta. Việc lưu trữ các tính năng này giúp bạn có thể sử dụng nhiều tính năng thú vị. Ví dụ: bạn nên lưu trữ vị trí cho một việc cần làm để ứng dụng có thể nhắc bạn về việc đó khi bạn đến một địa điểm.
Cloud Firestore có kiểu dữ liệu tích hợp là GeoPoint
, có thể lưu trữ kinh độ và vĩ độ của mọi vị trí. Để lập bản đồ vị trí từ/đến một 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 về 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; chúng còn nhiều điều thú vị hơn bạn nghĩ. Một trường hợp sử dụng phổ biến cho enum là mô hình hoá các trạng thái rời rạc của một thứ gì đó. Ví dụ: chúng ta có thể đang viết một ứng dụng để quản lý các bài viết. Để theo dõi trạng thái của một bài viết, chúng ta có thể muốn 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ợ các enum một cách tự nhiên (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 thực tế là các enum có thể được nhập và chọn một loại có thể mã hoá. Trong ví dụ này, chúng ta đã chọn String
, tức là tất cả các giá trị enum sẽ được ánh xạ đến/từ chuỗi khi được lưu trữ trong tài liệu Cloud Firestore.
Ngoài ra, 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 những 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
ở trạng thái "đ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 các 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 lắng: Codable sẽ giúp chúng ta!
Đối với các trường hợp như vậ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ể mã hoá để chỉ định cách ánh xạ một số thuộc tính nhất định.
Hãy xem xét tài liệu này:
Để ánh xạ tài liệu này đến 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
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 liên kết. Vì vậy, miễn là tên thuộc tính khớp, bạn không cần thêm CodingKeys
vào các loại có thể mã hoá của chúng tôi. 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 dùng làm giá trị nhận dạng trong khung hiển thị List
của SwiftUI. Nếu chúng ta không chỉ định nó trong CodingKeys
, thì nó sẽ không được liên kết khi tìm nạp dữ liệu và do đó trở thành nil
.
Điều này sẽ dẫn đến việc khung hiển thị List
được điền bằng tài liệu đầu tiên.
Mọi thuộc tính 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 có thể thực sự 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 được liên kết.
Ví dụ: nếu muốn loại trừ thuộc tính reasonWhyILoveThis
khỏi việc được ánh xạ, 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ề giá trị tuỳ chọn để biểu thị sự thiếu vắng 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ã hoá các giá trị không bắt buộc có giá trị nil
là chỉ bỏ qua các giá trị đó. @ExplicitNull
cho phép chúng ta kiểm soát cách các giá trị tuỳ chọn Swift được xử lý khi mã hoá: bằng cách gắn cờ một thuộc tính tuỳ 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 với giá trị rỗng nếu thuộc tính đó chứa giá trị nil
.
Sử dụng bộ mã hoá và bộ giải mã tuỳ chỉnh để ánh xạ màu
Là chủ đề cuối cùng trong phạm vi đề cập đến việc lập bản đồ dữ liệu bằng Codable, hãy giới thiệu bộ mã hoá và bộ giải mã tuỳ chỉnh. Phần này không đề cập đến kiểu dữ liệu Cloud Firestore gốc, nhưng 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" 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 để ánh xạ 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 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 đơn giản và hiệu quả 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 hệ thập lục phân CSS) – chúng rất 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 một Color
Swift với giá trị thập lục phân 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ã một 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 phải chuyển đổi trước!
Nhờ đó, chúng ta có thể cập nhật mã để ánh xạ các thẻ, giúp việc xử lý màu thẻ trực tiếp trở nên dễ dàng hơn thay vì phải ánh xạ các thẻ 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ố ý giữ cho việc xử lý lỗi ở mức tối thiểu, nhưng trong một ứng dụng phát hành công khai, bạn sẽ muốn đảm bảo xử lý mọi lỗi một cách thích hợp.
Dưới đây là một đoạn mã cho biết cách xử lý mọi trường hợp 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 thông tin cập nhật trực tiếp
Đ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ợ gửi 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 ta có thể đăng ký một 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 trình nghe của chúng ta bất cứ khi nào có bản cập nhật.
Dưới đây là một đoạn mã cho biết cách đăng ký một 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. Ví dụ này cũng cho thấy cách thêm một tài liệu mới vào bộ sưu tập. Như bạn thấy, chúng ta không cần tự cập nhật mảng cục bộ chứa các tài liệu được liên kết, vì mã trong trình nghe dữ liệu tức thời 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ả đoạn mã được dùng trong bài đăng này đều thuộc một ứng dụng mẫu mà bạn có thể tải xuống qua kho lưu trữ GitHub này.
Hãy bắt đầu và sử dụng Codable!
Codable API của Swift cung cấp một cách thức mạnh mẽ và linh hoạt để liên kết dữ liệu từ các định dạng được chuyển đổi tuần tự đến và đi từ mô hình dữ liệu của ứ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 dùng Cloud Firestore làm kho lưu trữ 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 đã 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 Codable và việc triển khai của Firebase để thực hiện việc ánh xạ cho chúng tôi.
Để 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:
- John Sundell có một bài viết hay về Basics of Codable (Kiến thức cơ bản về Codable).
- Nếu bạn thích đọc sách hơn, hãy xem Hướng dẫn về Swift Codable của Flight School của Mattt.
- Và cuối cùng, Donny Wals có một loạt bài về Codable.
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ề việc lập bản đồ 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 để lập bản đồ các loại của mình. Bằng cách sử dụng nút Gửi ý kiến phản hồi bên dưới, hãy cho chúng tôi biết những chiến lược mà 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.