Cloud Firestore iOS Codelab

מטרות

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

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

תנאים מוקדמים

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

  • גרסת Xcode 8.3 (ומעלה)
  • CocoaPods 1.2.1 (ומעלה)

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

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

הורד את הקוד

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

10a0671ce8f99704.png

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

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

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

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

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

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

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

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

2ca7f8c6052f7f79.png

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

הנה דוגמה לשאילתה פשוטה להביא את כל המסעדות של דים סאם:

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

כשמו כן הוא, שיטת whereField(_:isEqualTo:) תגרום לשאילתה שלנו להוריד רק את חברי האוסף 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/testapp-5d356/database/firestore/indexes?create_index=..." 
UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_index=...}

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

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

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

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

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

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

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

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