Codable API של Swift, שהושק ב-Swift 4, מאפשר לנו להשתמש ביכולות של הקומפיילר כדי למפות נתונים מפורמטים מסודרים לסוגי Swift בצורה קלה יותר.
יכול להיות שהשתמשתם ב-Codable כדי למפות נתונים מ-API של אינטרנט למודל הנתונים של האפליקציה (ולהיפך), אבל הוא הרבה יותר גמיש מזה.
במדריך הזה נסביר איך אפשר להשתמש ב-Codable כדי למפות נתונים מ-Cloud Firestore לסוגי Swift ולהיפך.
כשמאחזרים מסמך מ-Cloud Firestore, האפליקציה מקבלת מילון של צמדי מפתח/ערך (או מערך של מילונים, אם משתמשים באחת מהפעולות שמחזירות כמה מסמכים).
אפשר להמשיך להשתמש ישירות במילונים ב-Swift, והם מציעים גמישות רבה שעשויה להתאים בדיוק לתרחיש השימוש שלכם. עם זאת, הגישה הזו לא בטוחה מבחינת טיפוס, וקל להכניס באגים שקשה לאתר אם מאייתים לא נכון את שמות המאפיינים או שוכחים למפות את המאפיין החדש שהצוות הוסיף כשפיתח את התכונה החדשה והמרגשת בשבוע שעבר.
בעבר, מפתחים רבים עקפו את החסרונות האלה באמצעות הטמעה של שכבת מיפוי פשוטה שאיפשרה להם למפות מילונים לסוגי Swift. אבל שוב, רוב ההטמעות האלה מבוססות על הגדרה ידנית של המיפוי בין Cloud Firestoreמסמכים לבין הסוגים התואמים של מודל הנתונים של האפליקציה.
התמיכה של Cloud Firestore ב-Codable API של Swift הופכת את התהליך להרבה יותר פשוט:
- לא תצטרכו יותר להטמיע קוד מיפוי באופן ידני.
- קל להגדיר איך למפות מאפיינים עם שמות שונים.
- יש בו תמיכה מובנית בסוגים רבים של Swift.
- בנוסף, קל להוסיף תמיכה במיפוי סוגים מותאמים אישית.
- והכי חשוב: במודלים פשוטים של נתונים, לא תצטרכו לכתוב קוד מיפוי בכלל.
מיפוי נתונים
Cloud Firestore מאחסן נתונים במסמכים שממפים מפתחות לערכים. כדי לאחזר נתונים ממסמך ספציפי, אפשר לקרוא ל-DocumentSnapshot.data()
, שמחזירה מילון עם מיפוי של שמות השדות ל-Any
:
func data() -> [String : Any]?
.
כלומר, אפשר להשתמש בתחביר של subscript ב-Swift כדי לגשת לכל שדה בנפרד.
import FirebaseFirestore
#warning("DO NOT MAP YOUR DOCUMENTS MANUALLY. USE CODABLE INSTEAD.")
func fetchBook(documentId: String) {
let docRef = db.collection("books").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
let id = document.documentID
let data = document.data()
let title = data?["title"] as? String ?? ""
let numberOfPages = data?["numberOfPages"] as? Int ?? 0
let author = data?["author"] as? String ?? ""
self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author)
}
}
}
}
למרות שנדמה שהקוד הזה פשוט וקל להטמעה, הוא שביר, קשה לתחזוקה ונוטה לשגיאות.
כמו שאפשר לראות, אנחנו מניחים הנחות לגבי סוגי הנתונים של השדות במסמך. יכול להיות שהם נכונים ויכול להיות שלא.
חשוב לזכור: מכיוון שאין סכימה, אפשר בקלות להוסיף מסמך חדש לאוסף ולבחור סוג אחר לשדה. יכול להיות שתבחרו בטעות במחרוזת בשדה numberOfPages
, וכתוצאה מכך תהיה בעיה במיפוי שקשה למצוא. בנוסף, תצטרכו לעדכן את קוד המיפוי בכל פעם שתוסיפו שדה חדש, וזה די מסורבל.
בנוסף, אנחנו לא מנצלים את מערכת הטיפוס החזקה של Swift, שיודעת בדיוק מה הסוג הנכון של כל אחת מהמאפיינים של Book
.
מה זה Codable?
לפי התיעוד של Apple, Codable הוא "סוג שיכול להמיר את עצמו לייצוג חיצוני וממנו". למעשה, Codable הוא כינוי לסוג של הפרוטוקולים Encodable ו-Decodable. אם מקיימים את הפרוטוקול הזה בסוג Swift, הקומפיילר יבצע סינתזה של הקוד שנדרש כדי לקודד/לפענח מופע של הסוג הזה מפורמט סריאלי, כמו JSON.
סוג פשוט לאחסון נתונים על ספר יכול להיראות כך:
struct Book: Codable {
var title: String
var numberOfPages: Int
var author: String
}
כמו שאפשר לראות, התאמת הסוג ל-Codable היא פולשנית במידה מינימלית. היינו צריכים רק להוסיף את התאימות לפרוטוקול, ולא נדרשו שינויים נוספים.
אחרי שקבענו את זה, אנחנו יכולים לקודד בקלות ספר לאובייקט JSON:
do {
let book = Book(title: "The Hitchhiker's Guide to the Galaxy",
numberOfPages: 816,
author: "Douglas Adams")
let encoder = JSONEncoder()
let data = try encoder.encode(book)
}
catch {
print("Error when trying to encode book: \(error)")
}
פענוח של אובייקט JSON למופע Book
מתבצע באופן הבא:
let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)
מיפוי לסוגים פשוטים ומסוגים פשוטים בCloud Firestore מסמכים
באמצעות Codable
Cloud Firestore תומך במגוון רחב של סוגי נתונים, החל ממחרוזות פשוטות ועד למפות מקוננות. רובם תואמים ישירות לסוגים המובנים של Swift. לפני שנעבור לסוגי נתונים מורכבים יותר, נתחיל עם מיפוי של כמה סוגי נתונים פשוטים.
כדי למפות מסמכי Cloud Firestore לסוגי Swift, פועלים לפי השלבים הבאים:
- מוודאים שהוספתם את מסגרת
FirebaseFirestore
לפרויקט. אפשר להשתמש בSwift Package Manager או ב-CocoaPods. - מייבאים את
FirebaseFirestore
לקובץ Swift. - התאמת הסוג ל-
Codable
. - (אופציונלי, אם רוצים להשתמש בסוג בתצוגה
List
) מוסיפים מאפייןid
לסוג, ומשתמשים ב-@DocumentID
כדי להגדיר ל-Cloud Firestore מיפוי של המאפיין הזה למזהה המסמך. בהמשך נרחיב בנושא הזה. - אפשר להשתמש ב-
documentReference.data(as: )
כדי למפות הפניה למסמך לסוג Swift. - משתמשים ב-
documentReference.setData(from: )
כדי למפות נתונים מסוגי Swift למסמך Cloud Firestore. - (אופציונלי, אבל מומלץ מאוד) הטמעת טיפול נכון בשגיאות.
לכן, נעדכן את סוג Book
בהתאם:
struct Book: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
}
מכיוון שהסוג הזה כבר היה ניתן לקידוד, נדרשנו רק להוסיף את המאפיין id
ולציין אותו באמצעות עטיפת המאפיין @DocumentID
.
אם ניקח את קטע הקוד הקודם לאחזור ולמיפוי של מסמך, נוכל להחליף את כל קוד המיפוי הידני בשורה אחת:
func fetchBook(documentId: String) {
let docRef = db.collection("books").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.book = try document.data(as: Book.self)
}
catch {
print(error)
}
}
}
}
}
אפשר לכתוב את זה בצורה תמציתית יותר על ידי ציון סוג המסמך כשקוראים ל-getDocument(as:)
. הפעולה הזו תבצע את המיפוי ותחזיר סוג Result
שמכיל את המסמך הממופה, או שגיאה אם פענוח הקידוד נכשל:
private func fetchBook(documentId: String) {
let docRef = db.collection("books").document(documentId)
docRef.getDocument(as: Book.self) { result in
switch result {
case .success(let book):
// A Book value was successfully initialized from the DocumentSnapshot.
self.book = book
self.errorMessage = nil
case .failure(let error):
// A Book value could not be initialized from the DocumentSnapshot.
self.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
}
}
כדי לעדכן מסמך קיים, פשוט קוראים לפונקציה
documentReference.setData(from: )
. הנה קוד לשמירת מופע של Book
, כולל טיפול בסיסי בשגיאות:
func updateBook(book: Book) {
if let id = book.id {
let docRef = db.collection("books").document(id)
do {
try docRef.setData(from: book)
}
catch {
print(error)
}
}
}
כשמוסיפים מסמך חדש, Cloud Firestore דואג באופן אוטומטי להקצאת מזהה מסמך חדש למסמך. האפשרות הזו פועלת גם כשהאפליקציה במצב אופליין.
func addBook(book: Book) {
let collectionRef = db.collection("books")
do {
let newDocReference = try collectionRef.addDocument(from: self.book)
print("Book stored with new document reference: \(newDocReference)")
}
catch {
print(error)
}
}
בנוסף למיפוי של סוגי נתונים פשוטים, Cloud Firestore תומך במספר סוגים אחרים של נתונים, חלקם סוגים מובנים שבהם אפשר להשתמש כדי ליצור אובייקטים מקוננים בתוך מסמך.
סוגים מותאמים אישית מקוננים
רוב המאפיינים שאנחנו רוצים למפות במסמכים הם ערכים פשוטים, כמו שם הספר או שם המחבר. אבל מה קורה במקרים שבהם אנחנו צריכים לאחסן אובייקט מורכב יותר? לדוגמה, יכול להיות שנרצה לאחסן את כתובות ה-URL של כריכת הספר ברזולוציות שונות.
הדרך הקלה ביותר לעשות זאת ב-Cloud Firestore היא באמצעות מפה:
כשכותבים את מבנה ה-Swift המתאים, אפשר להשתמש בעובדה ש-Cloud Firestore תומך בכתובות URL – כשמאחסנים שדה שמכיל כתובת URL, הוא יומר למחרוזת ולהפך:
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?
}
שימו לב לאופן שבו הגדרנו מבנה, CoverImages
, למפת הכיסוי במסמך Cloud Firestore. סימנו את מאפיין הכריכה ב-BookWithCoverImages
כמאפיין אופציונלי, כדי שנוכל לטפל במצב שבו חלק מהמסמכים לא מכילים מאפיין כריכה.
אם אתם רוצים לדעת למה אין קטע קוד לאחזור או לעדכון נתונים, תשמחו לשמוע שאין צורך להתאים את הקוד לקריאה או לכתיבה מ-Cloud Firestore או אל Cloud Firestore: הכול עובד עם הקוד שכתבנו בקטע הראשון.
מערכים
לפעמים אנחנו רוצים לאחסן אוסף של ערכים במסמך. ז'אנרים של ספר הם דוגמה טובה: ספר כמו מדריך הטרמפיסט לגלקסיה יכול להשתייך לכמה קטגוריות – במקרה הזה, 'מדע בדיוני' ו'קומדיה':
ב-Cloud Firestore, אפשר ליצור מודל של זה באמצעות מערך של ערכים. התמיכה הזו זמינה לכל סוג שניתן לקידוד (כמו String
, Int
וכו'). בדוגמה הבאה אפשר לראות איך מוסיפים מערך של ז'אנרים למודל Book
:
public struct BookWithGenre: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
var genres: [String]
}
מכיוון שהפעולה הזו מתאימה לכל סוג שניתן לקידוד, אפשר להשתמש גם בסוגים מותאמים אישית. נניח שאנחנו רוצים לשמור רשימה של תגים לכל ספר. בנוסף לשם התג, אנחנו רוצים לאחסן גם את הצבע של התג, כך:
כדי לאחסן תגים בדרך הזו, צריך להטמיע מבנה Tag
כדי לייצג תג ולהפוך אותו לניתן לקידוד:
struct Tag: Codable, Hashable {
var title: String
var color: String
}
וכך אפשר לאחסן מערך של Tags
במסמכי Book
!
struct BookWithTags: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
var tags: [Tag]
}
הסבר קצר על מיפוי מזהי מסמכים
לפני שנעבור למיפוי של סוגים נוספים, נדבר רגע על מיפוי של מזהי מסמכים.
השתמשנו במעטפת המאפיין @DocumentID
בחלק מהדוגמאות הקודמות כדי למפות את מזהה המסמך של מסמכי Cloud Firestore למאפיין id
של סוגי Swift. הדבר חשוב מכמה סיבות:
- המידע הזה עוזר לנו לדעת איזה מסמך לעדכן אם המשתמש מבצע שינויים מקומיים.
- המאפיין
List
של SwiftUI מחייב שהרכיבים שלו יהיוIdentifiable
כדי למנוע מצב שבו הרכיבים קופצים ממקום למקום כשהם מוכנסים.
חשוב לציין שמאפיין שמסומן כ-@DocumentID
לא יקודד על ידי המקודד של Cloud Firestore כשכותבים את המסמך בחזרה. הסיבה לכך היא שמזהה המסמך הוא לא מאפיין של המסמך עצמו, ולכן יהיה זה שגיאה לכתוב אותו במסמך.
כשעובדים עם סוגים מקוננים (כמו מערך התגים ב-Book
בדוגמה מוקדמת יותר במדריך הזה), אין צורך להוסיף מאפיין @DocumentID
: מאפיינים מקוננים הם חלק ממסמך Cloud Firestore, ולא מהווים מסמך נפרד. לכן, לא צריך להגדיר בשבילן מזהה מסמך.
תאריכים ושעות
ל-Cloud Firestore יש סוג נתונים מובנה לטיפול בתאריכים ובשעות, ובזכות התמיכה של Cloud Firestore ב-Codable, קל להשתמש בהם.
בואו נסתכל על המסמך הזה שמייצג את השפה Ada, שפת האם של כל שפות התכנות, שהומצאה בשנת 1843:
סוג Swift למיפוי המסמך הזה יכול להיראות כך:
struct ProgrammingLanguage: Codable {
@DocumentID var id: String?
var name: String
var year: Date
}
אי אפשר לסיים את החלק הזה על תאריכים ושעות בלי לדבר על @ServerTimestamp
. עטיפת המאפיין הזו היא כלי רב עוצמה כשמדובר בטיפול בחותמות זמן באפליקציה.
בכל מערכת מבוזרת, סביר להניח שהשעונים במערכות הפרטניות לא מסונכרנים לחלוטין כל הזמן. יכול להיות שזה לא נראה לכם עניין גדול, אבל תחשבו על ההשלכות של שעון שלא מסונכרן באופן מדויק במערכת למסחר במניות: אפילו סטייה של אלפית השנייה יכולה לגרום להבדל של מיליוני דולרים כשמבצעים מסחר.
Cloud Firestore מטפל במאפיינים שמסומנים ב-@ServerTimestamp
באופן הבא: אם המאפיין הוא nil
כשמאחסנים אותו (באמצעות addDocument()
, לדוגמה), Cloud Firestore יאכלס את השדה עם חותמת הזמן הנוכחית של השרת בזמן הכתיבה למסד הנתונים. אם השדה לא nil
כשמבצעים קריאה ל-addDocument()
או ל-updateData()
, הפונקציה Cloud Firestore לא תשנה את ערך המאפיין. כך קל להטמיע שדות כמו createdAt
ו-lastUpdatedAt
.
נקודות גיאוגרפיות
מיקומים גיאוגרפיים נמצאים בכל האפליקציות שלנו. אפשר לאחסן אותם כדי להשתמש בהרבה תכונות מעניינות. לדוגמה, יכול להיות שיהיה לכם שימושי לשמור מיקום למשימה, כדי שהאפליקציה תוכל להזכיר לכם לגבי משימה כשתגיעו ליעד.
ל-Cloud Firestore יש סוג נתונים מובנה, GeoPoint
, שיכול לאחסן את קו האורך וקו הרוחב של כל מיקום. כדי למפות מיקומים ממסמך Cloud Firestore או אליו, אפשר להשתמש בסוג GeoPoint
:
struct Office: Codable {
@DocumentID var id: String?
var name: String
var location: GeoPoint
}
הסוג המתאים ב-Swift הוא CLLocationCoordinate2D
, ואפשר למפות בין שני הסוגים האלה באמצעות הפעולה הבאה:
CLLocationCoordinate2D(latitude: office.location.latitude,
longitude: office.location.longitude)
במדריך הפתרונות הזה מוסבר איך לשלוח שאילתות למסמכים לפי מיקום פיזי.
טיפוסים בני מנייה (enum)
סוגי ה-enum הם כנראה אחת התכונות הכי לא מוערכות בשפת Swift. יש בהם הרבה יותר ממה שנראה במבט ראשון. תרחיש שימוש נפוץ בסוגי enum הוא
הדמיה של מצבים נפרדים של משהו. לדוגמה, אנחנו יכולים לכתוב אפליקציה לניהול מאמרים. כדי לעקוב אחרי הסטטוס של מאמר, כדאי להשתמש ב-enum Status
:
enum Status: String, Codable {
case draft
case inReview
case approved
case published
}
Cloud Firestore לא תומך ב-enums באופן מקורי (כלומר, הוא לא יכול לאכוף את קבוצת הערכים), אבל עדיין אפשר להשתמש בעובדה שאפשר להקליד enums ולבחור סוג ניתן לקידוד. בדוגמה הזו בחרנו באפשרות String
, כלומר כל ערכי ה-enum ימופו למחרוזת או ממחרוזת כשהם מאוחסנים במסמך Cloud Firestore.
בנוסף, מכיוון ש-Swift תומכת בערכים גולמיים מותאמים אישית, אנחנו יכולים אפילו להתאים אישית את הערכים שמתייחסים לכל מקרה של enum. לדוגמה, אם החלטנו לאחסן את מקרה Status.inReview
כ'בבדיקה', אנחנו יכולים לעדכן את סוג הנתונים שצוין למעלה באופן הבא:
enum Status: String, Codable {
case draft
case inReview = "in review"
case approved
case published
}
התאמה אישית של המיפוי
לפעמים, שמות המאפיינים של Cloud Firestore המסמכים שאנחנו רוצים למפות לא תואמים לשמות המאפיינים במודל הנתונים שלנו ב-Swift. לדוגמה, אחד מהעובדים שלנו הוא מפתח Python, והוא החליט להשתמש בפורמט snake_case לכל שמות המאפיינים.
אל דאגה: Codable דואג לנו!
במקרים כאלה, אפשר להשתמש ב-CodingKeys
. זוהי ספירה שאפשר להוסיף למבנה ניתן לקידוד כדי לציין איך מאפיינים מסוימים ימופו.
קרא את המסמך הבא:
כדי למפות את המסמך הזה למבנה (struct) שיש לו מאפיין שם מסוג String
, צריך להוסיף ספירה (enum) CodingKeys
למבנה ProgrammingLanguage
ולציין את שם המאפיין במסמך:
struct ProgrammingLanguage: Codable {
@DocumentID var id: String?
var name: String
var year: Date
enum CodingKeys: String, CodingKey {
case id
case name = "language_name"
case year
}
}
כברירת מחדל, Codable API ישתמש בשמות המאפיינים של סוגי Swift כדי לקבוע את שמות המאפיינים במסמכי Cloud Firestore שאנחנו מנסים למפות. לכן, כל עוד שמות המאפיינים זהים, אין צורך להוסיף את CodingKeys
לסוגים הניתנים לקידוד שלנו. עם זאת, אחרי שמשתמשים ב-CodingKeys
לסוג מסוים, צריך להוסיף את כל שמות המאפיינים שרוצים למפות.
בקטע הקוד שלמעלה, הגדרנו מאפיין id
שאולי נרצה להשתמש בו כמזהה בתצוגת List
של SwiftUI. אם לא ציינו אותו ב-CodingKeys
, הוא לא ימופה בזמן אחזור הנתונים, ולכן יהפוך ל-nil
.
המסמך הראשון יופיע בתצוגה List
.
כל נכס שלא מופיע כ-case ב-CodingKeys
enum
המתאים יתעלמו ממנו במהלך תהליך המיפוי. האפשרות הזו יכולה להיות נוחה אם רוצים להחריג מלונות מסוימים מהמיפוי.
לדוגמה, אם רוצים להחריג את הנכס reasonWhyILoveThis
ממיפוי, כל מה שצריך לעשות הוא להסיר אותו מה-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
}
}
לפעמים נרצה לכתוב מאפיין ריק בחזרה למסמך Cloud Firestore. ב-Swift יש את המושג optionals שמציין היעדר ערך, ו-Cloud Firestore תומך גם בערכים null
.
עם זאת, התנהגות ברירת המחדל של קידוד אופציונלי עם ערך nil
היא פשוט להשמיט אותם. המאפיין @ExplicitNull
מאפשר לנו לשלוט באופן הטיפול בערכים אופציונליים ב-Swift בזמן הקידוד שלהם: אם נסמן מאפיין אופציונלי כ-@ExplicitNull
, נוכל להנחות את Cloud Firestore לכתוב את המאפיין הזה במסמך עם ערך null אם הוא מכיל ערך של nil
.
שימוש במקודד ובמפענח מותאמים אישית למיפוי צבעים
כנושא אחרון בסקירה שלנו על מיפוי נתונים באמצעות Codable, נציג מקודדים ומפענחים מותאמים אישית. בקטע הזה לא מוסבר על Cloud Firestoreסוג נתונים מקורי, אבל מקודדים ומפענחים בהתאמה אישית שימושיים מאוד באפליקציות Cloud Firestore.
'איך ממפים צבעים' היא אחת השאלות הנפוצות ביותר בקרב מפתחים, לא רק לגבי Cloud Firestore, אלא גם לגבי מיפוי בין Swift ל-JSON. יש הרבה פתרונות, אבל רובם מתמקדים ב-JSON, וכמעט כולם ממפים צבעים כמילון מקונן שמורכב מרכיבי ה-RGB שלו.
נראה שיש פתרון טוב ופשוט יותר. למה אנחנו לא משתמשים בצבעי אינטרנט (או ליתר דיוק, בסימון צבעים הקסדצימליים של CSS)? קל להשתמש בהם (בעצם מדובר רק במחרוזת), והם אפילו תומכים בשקיפות!
כדי למפות מחרוזת Swift Color
לערך ההקסדצימלי שלה, צריך ליצור תוסף Swift שמוסיף Codable ל-Color
.
extension Color {
init(hex: String) {
let rgba = hex.toRGBA()
self.init(.sRGB,
red: Double(rgba.r),
green: Double(rgba.g),
blue: Double(rgba.b),
opacity: Double(rgba.alpha))
}
//... (code for translating between hex and RGBA omitted for brevity)
}
extension Color: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let hex = try container.decode(String.self)
self.init(hex: hex)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(toHex)
}
}
באמצעות decoder.singleValueContainer()
, אפשר לפענח String
למקבילה של Color
, בלי להטמיע את רכיבי ה-RGBA. בנוסף, אפשר להשתמש בערכים האלה בממשק המשתמש האינטרנטי של האפליקציה בלי להמיר אותם קודם.
כך אנחנו יכולים לעדכן את הקוד למיפוי תגים, ולטפל בצבעי התגים ישירות במקום למפות אותם ידנית בקוד של ממשק המשתמש של האפליקציה:
struct Tag: Codable, Hashable {
var title: String
var color: Color
}
struct BookWithTags: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
var tags: [Tag]
}
טיפול בשגיאות
בקטעי הקוד שלמעלה השתדלנו לצמצם את הטיפול בשגיאות, אבל באפליקציה בסביבת ייצור, כדאי לטפל בכל השגיאות בצורה חלקה.
קטע הקוד הבא מראה איך לטפל במצבי שגיאה שונים שאולי יקרו:
class MappingSimpleTypesViewModel: ObservableObject {
@Published var book: Book = .empty
@Published var errorMessage: String?
private var db = Firestore.firestore()
func fetchAndMap() {
fetchBook(documentId: "hitchhiker")
}
func fetchAndMapNonExisting() {
fetchBook(documentId: "does-not-exist")
}
func fetchAndTryMappingInvalidData() {
fetchBook(documentId: "invalid-data")
}
private func fetchBook(documentId: String) {
let docRef = db.collection("books").document(documentId)
docRef.getDocument(as: Book.self) { result in
switch result {
case .success(let book):
// A Book value was successfully initialized from the DocumentSnapshot.
self.book = book
self.errorMessage = nil
case .failure(let error):
// A Book value could not be initialized from the DocumentSnapshot.
switch error {
case DecodingError.typeMismatch(_, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self.errorMessage = "\(error.localizedDescription): \(key)"
default:
self.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
}
}
}
}
טיפול בשגיאות בעדכונים בזמן אמת
בקטע הקוד הקודם מוצגות דרכים לטפל בשגיאות כשמאחזרים מסמך יחיד. בנוסף לאחזור נתונים חד-פעמי, Cloud Firestore תומך גם בהעברת עדכונים לאפליקציה בזמן שהם מתרחשים, באמצעות מה שנקרא snapshot listeners: אפשר לרשום snapshot listener בקולקציה (או בשאילתה), ו-Cloud Firestore יפעיל את ה-listener שלנו בכל פעם שיש עדכון.
קטע הקוד הבא מראה איך לרשום מאזין לתמונת מצב, למפות נתונים באמצעות Codable ולטפל בשגיאות שעלולות להתרחש. בנוסף, מוסבר איך להוסיף מסמך חדש לאוסף. כפי שניתן לראות, אין צורך לעדכן את המערך המקומי שמכיל את המסמכים הממופים בעצמנו, כי הקוד במאזין של התמונה המיידית מטפל בכך.
class MappingColorsViewModel: ObservableObject {
@Published var colorEntries = [ColorEntry]()
@Published var newColor = ColorEntry.empty
@Published var errorMessage: String?
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
public func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("colors")
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.errorMessage = "No documents in 'colors' collection"
return
}
self?.colorEntries = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: ColorEntry.self) }
switch result {
case .success(let colorEntry):
if let colorEntry = colorEntry {
// A ColorEntry value was successfully initialized from the DocumentSnapshot.
self?.errorMessage = nil
return colorEntry
}
else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
self?.errorMessage = "Document doesn't exist."
return nil
}
case .failure(let error):
// A ColorEntry value could not be initialized from the DocumentSnapshot.
switch error {
case DecodingError.typeMismatch(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self?.errorMessage = "\(error.localizedDescription): \(key)"
default:
self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
return nil
}
}
}
}
}
func addColorEntry() {
let collectionRef = db.collection("colors")
do {
let newDocReference = try collectionRef.addDocument(from: newColor)
print("ColorEntry stored with new document reference: \(newDocReference)")
}
catch {
print(error)
}
}
}
כל קטעי הקוד שמופיעים בפוסט הזה הם חלק מאפליקציה לדוגמה שאפשר להוריד ממאגר GitHub הזה.
קדימה, אפשר להשתמש ב-Codable!
Codable API של Swift מספק דרך עוצמתית וגמישה למפות נתונים מפורמטים מסודרים אל מודל הנתונים של האפליקציות שלכם וממנו. במדריך הזה ראיתם כמה קל להשתמש ב-Cloud Firestore באפליקציות שמשתמשות בו כמאגר נתונים.
התחלנו עם דוגמה בסיסית עם סוגי נתונים פשוטים, והגדלנו בהדרגה את המורכבות של מודל הנתונים, תוך הסתמכות על Codable וההטמעה של Firebase כדי לבצע את המיפוי בשבילנו.
לפרטים נוספים על Codable, מומלץ לעיין במקורות המידע הבאים:
- במאמר של ג'ון סאנדל מוסבר על היסודות של Codable.
- אם אתם מעדיפים ספרים, כדאי לעיין במדריך של Mattt בנושא Swift Codable.
- ולבסוף, ל-Donny Wals יש סדרה שלמה על Codable.
עשינו כמיטב יכולתנו כדי ליצור מדריך מקיף למיפוי מסמכי Cloud Firestore, אבל הוא לא ממצה את כל האפשרויות, ויכול להיות שאתם משתמשים באסטרטגיות אחרות למיפוי הסוגים. נשמח לקבל מכם משוב. אפשר ללחוץ על הלחצן שליחת משוב שבהמשך ולספר לנו באילו שיטות אתם משתמשים כדי למפות סוגים אחרים של נתוני Cloud Firestore או כדי לייצג נתונים ב-Swift.
אין סיבה שלא להשתמש בתמיכה של Cloud Firestore ב-Codable.