Cloud Firestore iOS Codelab

Codelab ל-Cloud Firestore ל-iOS

מידע על Codelab זה

subjectהעדכון האחרון: ינו׳ 23, 2025
account_circleנכתב על ידי גוגלר

1.‏ סקירה כללית

מטרות עסקיות

בקודלאב הזה תלמדו איך ליצור אפליקציה לקבלת המלצות למסעדות ב-iOS בשפת Swift, שמבוססת על Firestore. תלמדו איך:

  1. קריאה וכתיבה של נתונים ב-Firestore מאפליקציה ל-iOS
  2. האזנה לשינויים בנתונים של Firestore בזמן אמת
  3. שימוש באימות ובכללי אבטחה של Firebase לאבטחת נתוני Firestore
  4. כתיבת שאילתות מורכבות ב-Firestore

דרישות מוקדמות

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

  • Xcode מגרסה 14.0 (או גרסה מתקדמת יותר)
  • CocoaPods 1.12.0 (או גרסה מתקדמת יותר)

2.‏ יצירת פרויקט במסוף Firebase

הוספת Firebase לפרויקט

  1. נכנסים למסוף Firebase.
  2. בוחרים באפשרות Create New Project (יצירת פרויקט חדש) ומזינים את השם Firestore iOS Codelab.

3.‏ הורדת פרויקט לדוגמה

הורדת הקוד

מתחילים בהעתקה (cloning) של הפרויקט לדוגמה והרצה של pod update בספריית הפרויקט:

git clone https://github.com/firebase/friendlyeats-ios
cd friendlyeats-ios
pod update

פותחים את FriendlyEats.xcworkspace ב-Xcode ומריצים אותו (Cmd+R). האפליקציה אמורה להתעבד בצורה תקינה ולקרוס מיידית בזמן ההפעלה, כי חסר בה קובץ GoogleService-Info.plist. נתקן את זה בשלב הבא.

הגדרת Firebase

פועלים לפי המסמכים כדי ליצור פרויקט חדש ב-Firestore. אחרי שתיצרו את הפרויקט, הורידו את קובץ ה-GoogleService-Info.plist של הפרויקט ממסוף Firebase וגררו אותו לתיקיית השורש של פרויקט Xcode. מריצים את הפרויקט שוב כדי לוודא שהאפליקציה מוגדרת בצורה נכונה ושאין יותר קריסות בזמן ההפעלה. אחרי הכניסה, אמור להופיע מסך ריק כמו בדוגמה הבאה. אם אתם לא מצליחים להתחבר, ודאו שהפעלתם את שיטת הכניסה באמצעות אימייל/סיסמה במסוף Firebase בקטע Authentication (אימות).

d5225270159c040b.png

4.‏ כתיבת נתונים ב-Firestore

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

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

כדי להוסיף נתונים ל-Firestore, אנחנו צריכים לקבל הפניה לאוסף המסעדות. מוסיפים את הטקסט הבא ל-for loop הפנימי בשיטה RestaurantsTableViewController.didTapPopulateButton(_:).

let collection = Firestore.firestore().collection("restaurants")

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

let collection = Firestore.firestore().collection("restaurants")

// ====== ADD THIS ======
let restaurant
= Restaurant(
  name
: name,
  category
: category,
  city
: city,
  price
: price,
  ratingCount
: 0,
  averageRating
: 0
)

collection
.addDocument(data: restaurant.dictionary)

הקוד שלמעלה מוסיף מסמך חדש לאוסף המסעדות. נתוני המסמך מגיעים ממילון, שאנחנו מקבלים מהמבנה Restaurant.

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

בכרטיסייה Rules במסוף Firebase, מוסיפים את הכללים הבאים ולוחצים על Publish.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{any}/ratings/{rating} {
      // Users can only write ratings with their user ID
      allow read;
      allow write: if request.auth != null
                   && request.auth.uid == request.resource.data.userId;
    }

    match /restaurants/{any} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

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

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

לאחר מכן, עוברים אל הכרטיסייה 'נתוני Firestore' במסוף Firebase. עכשיו אמורים להופיע רשומות חדשות בקולקציית המסעדות:

Screen Shot 2017-07-06 at 12.45.38 PM.png

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

5.‏ הצגת נתונים מ-Firestore

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

קודם נרכיב את השאילתה שתציג את רשימת המסעדות שמוגדרת כברירת מחדל, ללא סינון. עיינו בהטמעה של RestaurantsTableViewController.baseQuery():

return Firestore.firestore().collection("restaurants").limit(to: 50)

השאילתה הזו מאחזרת עד 50 מסעדות מהאוסף ברמה העליונה שנקרא 'restaurants'. עכשיו שיש לנו שאילתה, אנחנו צריכים לצרף מאזין לתמונה מצב כדי לטעון נתונים מ-Firestore לאפליקציה. מוסיפים את הקוד הבא לשיטה RestaurantsTableViewController.observeQuery() מיד אחרי הקריאה ל-stopObserving().

listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
  guard let snapshot = snapshot else {
    print("Error fetching snapshot results: \(error!)")
    return
  }
  let models = snapshot.documents.map { (document) -> Restaurant in
    if let model = Restaurant(dictionary: document.data()) {
      return model
    } else {
      // Don't use fatalError here in a real app.
      fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
    }
  }
  self.restaurants = models
  self.documents = snapshot.documents

  if self.documents.count > 0 {
    self.tableView.backgroundView = nil
  } else {
    self.tableView.backgroundView = self.backgroundView
  }

  self.tableView.reloadData()
}

הקוד שלמעלה מוריד את האוסף מ-Firestore ושומר אותו במערך באופן מקומי. הקריאה ל-addSnapshotListener(_:) מוסיפה לשאילתה מאזין לתמונה מיידית, שיעדכן את ה-View Controller בכל פעם שהנתונים משתנים בשרת. אנחנו מקבלים עדכונים באופן אוטומטי ולא צריכים לדחוף שינויים באופן ידני. חשוב לזכור שאפשר להפעיל את מאזין קובץ ה-snapshot הזה בכל שלב כתוצאה משינוי בצד השרת, ולכן חשוב שהאפליקציה שלנו תוכל לטפל בשינויים.

אחרי שממפים את המילונים שלנו למבנים (ראו Restaurant.swift), הצגת הנתונים היא רק עניין של הקצאה של כמה מאפייני תצוגה. מוסיפים את השורות הבאות לקובץ RestaurantTableViewCell.populate(restaurant:) בקובץ RestaurantsTableViewController.swift.

nameLabel.text = restaurant.name
cityLabel
.text = restaurant.city
categoryLabel
.text = restaurant.category
starsView
.rating = Int(restaurant.averageRating.rounded())
priceLabel
.text = priceString(from: restaurant.price)

שיטת האכלוס הזו נקראת מהשיטה tableView(_:cellForRowAtIndexPath:) של מקור הנתונים בתצוגת הטבלה, שמטפלת במיפוי האוסף של סוגי הערכים מהקודם לתאים הנפרדים בתצוגת הטבלה.

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

391c0259bf05ac25.png

6.‏ מיון וסינון של נתונים

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

דוגמה לשאילתה פשוטה לאחזור כל המסעדות שמגישות דים סאם:

let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")

כפי שרואים מהשם, השיטה whereField(_:isEqualTo:) תגרום לשאילתה שלנו להוריד רק את המשתתפים באוסף שהשדות שלהם עומדים בהגבלות שהגדרתם. במקרה כזה, המערכת תוריד רק מסעדות שבהן הערך של category הוא "Dim Sum".

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

פותחים את RestaurantsTableViewController.swift ומוסיפים את בלוק הקוד הבא באמצע query(withCategory:city:price:sortBy:):

if let category = category, !category.isEmpty {
  filtered
= filtered.whereField("category", isEqualTo: category)
}

if let city = city, !city.isEmpty {
  filtered
= filtered.whereField("city", isEqualTo: city)
}

if let price = price {
  filtered
= filtered.whereField("price", isEqualTo: price)
}

if let sortBy = sortBy, !sortBy.isEmpty {
  filtered
= filtered.order(by: sortBy)
}

קטע הקוד שלמעלה מוסיף כמה תנאים מסוג whereField ו-order כדי ליצור שאילתה מורכבת אחת על סמך הקלט של המשתמש. עכשיו השאילתה שלנו תחזיר רק מסעדות שתואמות לדרישות של המשתמש.

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

Error fetching snapshot results: Error Domain=io.grpc Code=9
"The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=..."
UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...}

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

7.‏ כתיבת נתונים בעסקה

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

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

למרבה המזל, Firestore מספק פונקציונליות של טרנזקציות שמאפשרת לנו לבצע מספר פעולות קריאה וכתיבה בפעולה אטומית אחת, וכך להבטיח שהנתונים שלנו יישארו עקביים.

מוסיפים את הקוד הבא מתחת לכל ההצהרות מסוג let ב-RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:).

let firestore = Firestore.firestore()
firestore
.runTransaction({ (transaction, errorPointer) -> Any? in

 
// Read data from Firestore inside the transaction, so we don't accidentally
 
// update using stale client data. Error if we're unable to read here.
  let restaurantSnapshot
: DocumentSnapshot
 
do {
   
try restaurantSnapshot = transaction.getDocument(reference)
 
} catch let error as NSError {
    errorPointer
?.pointee = error
   
return nil
 
}

 
// Error if the restaurant data in Firestore has somehow changed or is malformed.
  guard let data
= restaurantSnapshot.data(),
        let restaurant
= Restaurant(dictionary: data) else {

    let error
= NSError(domain: "FireEatsErrorDomain", code: 0, userInfo: [
     
NSLocalizedDescriptionKey: "Unable to write to restaurant at Firestore path: \(reference.path)"
   
])
    errorPointer
?.pointee = error
   
return nil
 
}

 
// Update the restaurant's rating and rating count and post the new review at the
 
// same time.
  let newAverage
= (Float(restaurant.ratingCount) * restaurant.averageRating + Float(review.rating))
     
/ Float(restaurant.ratingCount + 1)

  transaction
.setData(review.dictionary, forDocument: newReviewReference)
  transaction
.updateData([
   
"numRatings": restaurant.ratingCount + 1,
   
"avgRating": newAverage
 
], forDocument: reference)
 
return nil
}) { (object, error) in
 
if let error = error {
   
print(error)
 
} else {
   
// Pop the review controller on success
   
if self.navigationController?.topViewController?.isKind(of: NewReviewViewController.self) ?? false {
     
self.navigationController?.popViewController(animated: true)
   
}
 
}
}

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

8.‏ כללי אבטחה

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

קודם כול, נבחן לעומק את כללי האבטחה שכתבנו בתחילת הקודלאב. פותחים את מסוף Firebase ועוברים אל Database (מסד נתונים) > Rules (כללים) בכרטיסייה Firestore.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{any}/ratings/{rating} {
      // Users can only write ratings with their user ID
      allow read;
      allow write: if request.auth != null
                   && request.auth.uid == request.resource.data.userId;
    }

    match /restaurants/{any} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

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

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

משפט ההתאמה הראשון תואם לאוסף המשנה בשם ratings של כל מסמך ששייך לאוסף restaurants. התנאי allow write מונע שליחת ביקורת אם מזהה המשתמש של הביקורת לא תואם למזהה המשתמש. משפט ההתאמה השני מאפשר לכל משתמש מאומת לקרוא מסעדות ולכתוב אותן במסד הנתונים.

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

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{restaurant} {
      match /ratings/{rating} {
        allow read: if request.auth != null;
        allow write: if request.auth != null
                     && request.auth.uid == request.resource.data.userId;
      }

      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && request.resource.data.name == resource.data.name
                    && request.resource.data.city == resource.data.city
                    && request.resource.data.price == resource.data.price
                    && request.resource.data.category == resource.data.category;
    }
  }
}

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

מידע נוסף על הפעולות שאפשר לבצע באמצעות כללי אבטחה זמין במסמכי העזרה.

9.‏ סיכום

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

מידע נוסף על Firestore זמין במקורות המידע הבאים: