מפה נתונים של Cloud Firestore עם Swift Codable

ה-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]? .

המשמעות היא שנוכל להשתמש בתחביר המשנה של 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 , דבר שיוביל לבעיית מיפוי שקשה למצוא אותה. כמו כן, תצטרך לעדכן את קוד המיפוי שלך בכל פעם שמתווסף שדה חדש, וזה די מסורבל.

ובל נשכח שאנחנו לא מנצלים את מערכת הטיפוסים החזקה של סוויפט, שיודעת בדיוק את הסוג הנכון לכל אחד מהמאפיינים של Book .

מה זה Codable, בכלל?

על פי התיעוד של אפל, Codable הוא "טיפוס שיכול להמיר את עצמו לייצוג חיצוני ומחוצה לו". למעשה, Codable הוא כינוי מסוג עבור הפרוטוקולים הניתנים לקידוד ולפענוח. על ידי התאמה של סוג 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 תומך בקבוצה רחבה של סוגי נתונים, החל ממחרוזות פשוטות ועד מפות מקוננות. רובם מתאימים ישירות לסוגים המובנים של סוויפט. בואו נסתכל על מיפוי של כמה סוגי נתונים פשוטים תחילה לפני שנצלול אל המורכבים יותר.

כדי למפות מסמכי Cloud Firestore לסוגי Swift, בצע את השלבים הבאים:

  1. ודא שהוספת את מסגרת FirebaseFirestore לפרויקט שלך. אתה יכול להשתמש במנהל החבילות של Swift או ב-CocoaPods כדי לעשות זאת.
  2. ייבא את FirebaseFirestore לקובץ Swift שלך.
  3. התאם את הסוג שלך ל- Codable .
  4. (אופציונלי, אם ברצונך להשתמש בסוג בתצוגת List ) הוסף מאפיין id לסוג שלך, והשתמש @DocumentID כדי לומר ל-Cloud Firestore למפות את זה למזהה המסמך. נדון בכך ביתר פירוט להלן.
  5. השתמש documentReference.data(as: ) כדי למפות הפניה למסמך לסוג Swift.
  6. השתמש documentReference.setData(from: ) כדי למפות נתונים מסוגי Swift למסמך Cloud Firestore.
  7. (אופציונלי, אבל מומלץ מאוד) יישם טיפול נכון בשגיאות.

בואו נעדכן את סוג 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 תומך במספר סוגי נתונים אחרים, שחלקם הם סוגים מובנים שבהם אתה יכול להשתמש כדי ליצור אובייקטים מקוננים בתוך מסמך.

סוגים מותאמים אישית מקוננים

רוב התכונות שאנו רוצים למפות במסמכים שלנו הן ערכים פשוטים, כגון שם הספר או שם המחבר. אבל מה לגבי המקרים שבהם אנחנו צריכים לאחסן חפץ מורכב יותר? לדוגמה, ייתכן שנרצה לאחסן את כתובות האתרים לעטיפת הספר ברזולוציות שונות.

הדרך הקלה ביותר לעשות זאת ב-Cloud Firestore היא להשתמש במפה:

אחסון סוג מותאם אישית מקונן במסמך 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: כל זה עובד עם הקוד שאנו כתבתי בסעיף הראשוני.

מערכים

לפעמים, אנחנו רוצים לאחסן אוסף של ערכים במסמך. הז'אנרים של ספר הם דוגמה טובה: ספר כמו מדריך הטרמפיסט לגלקסיה עשוי להיכלל בכמה קטגוריות - במקרה הזה "מדע בדיוני" ו"קומדיה":

אחסון מערך במסמך 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]
}

מכיוון שזה עובד עבור כל סוג שניתן לקידוד, אנו יכולים להשתמש גם בסוגים מותאמים אישית. תאר לעצמך שאנחנו רוצים לאחסן רשימה של תגים עבור כל ספר. יחד עם שם התג, נרצה לאחסן גם את צבע התג, כך:

אחסון מערך של סוגים מותאמים אישית במסמך Firestore

כדי לאחסן תגים בצורה זו, כל שעלינו לעשות הוא ליישם מבנה 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, קל להשתמש בהם.

בואו נסתכל על המסמך הזה שמייצג את האם של כל שפות התכנות, עדה, שהומצאה ב-1843:

אחסון תאריכים במסמך Firestore

סוג 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)

למידע נוסף על שאילתת מסמכים לפי מיקום פיזי, עיין במדריך הפתרונות הזה .

תקצירים

Enums הם כנראה אחת מתכונות השפה המוערכות ביותר בסוויפט; יש בהם הרבה יותר ממה שנראה לעין. מקרה שימוש נפוץ עבור enums הוא מודל של מצבים בדידים של משהו. לדוגמה, ייתכן שאנו כותבים אפליקציה לניהול מאמרים. כדי לעקוב אחר הסטטוס של מאמר, אולי נרצה להשתמש Status enum :

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 . זהו מנה שאנו יכולים להוסיף למבנה הניתן לקידוד כדי לציין כיצד תכונות מסוימות ימופו.

שקול את המסמך הזה:

מסמך Firestore עם שם התכונה snake_cased

כדי למפות מסמך זה למבנה בעל מאפיין שם מסוג 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 המתאים יתעלם במהלך תהליך המיפוי. זה למעשה יכול להיות נוח אם אנחנו רוצים ספציפית לא לכלול חלק מהמאפיינים ממופה.

כך למשל, אם ברצוננו לא לכלול את המאפיין reasonWhyILoveThis ממופה, כל שעלינו לעשות הוא להסיר אותו מ- CodingKeys enum:

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. לסוויפט יש את הרעיון של אופציונליות לציון היעדר ערך, ו-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 לערך הhex שלו, עלינו ליצור הרחבה של 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 תומכת גם באספקת עדכונים לאפליקציה שלך בזמן שהם מתרחשים, באמצעות מה שנקרא מאזיני תמונת מצב: אנו יכולים לרשום מאזין תמונת מצב על אוסף (או שאילתה), ו-Cloud Firestore תתקשר למאזין שלנו בכל פעם שיש הוא עדכון.

להלן קטע קוד שמראה כיצד לרשום מאזין תמונת מצב, למפות נתונים באמצעות Codable ולטפל בכל שגיאה שעלולה להתרחש. זה גם מראה כיצד להוסיף מסמך חדש לאוסף. כפי שתראו, אין צורך לעדכן את המערך המקומי המחזיק את המסמכים הממופים בעצמנו, שכן הדבר מטופל על ידי הקוד במאזין ה-Snapshot.

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, אני ממליץ על המשאבים הבאים:

למרות שעשינו כמיטב יכולתנו להרכיב מדריך מקיף למיפוי מסמכי Cloud Firestore, זה לא ממצה, וייתכן שאתה משתמש באסטרטגיות אחרות למיפוי הסוגים שלך. באמצעות לחצן שלח משוב למטה, ספר לנו באילו אסטרטגיות אתה משתמש למיפוי סוגים אחרים של נתונים של Cloud Firestore או ייצוג נתונים ב-Swift.

אין באמת סיבה לא להשתמש בתמיכה ב-Codable של Cloud Firestore.