Cloud Firestore iOS Codelab

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

מטרות

במעבדת הקוד הזה תבנו אפליקציית המלצות למסעדות מגובות Firestore ב-iOS ב-Swift. תלמד כיצד:

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

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

לפני שתתחיל מעבדת קוד זה ודא שהתקנת:

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

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

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

  1. עבור אל מסוף Firebase .
  2. בחר צור פרויקט חדש ותן שם לפרויקט שלך "Firestore iOS Codelab".

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

הורד את הקוד

התחל בשיבוט הפרויקט לדוגמה והפעלת 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 תחת אימות.

d5225270159c040b.png

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

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

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

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

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

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

בכרטיסייה כללים של מסוף Firebase הוסף את הכללים הבאים ולאחר מכן לחץ על פרסם .

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

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

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

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

צילום מסך 2017-07-06 בשעה 12.45.38 PM.png

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

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

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

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

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

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

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

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

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

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

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

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

הצהרת ההתאמה הראשונה תואמת את 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;
    }
  }
}

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

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

9. מסקנה

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

למידע נוסף על Firestore, בקר במשאבים הבאים: