Codelab של Cloud Firestore iOS

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

מטרות

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

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

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

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

  • Xcode גרסה 8.3 (או יותר)
  • CocoaPods 1.2.1 (או יותר)

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

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

הגדר את Firebase

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

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

לאחר מיפוי המילונים שלנו structs (ראה 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!

2ca7f8c6052f7f79.png

6. מיון וסינון נתונים

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

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

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

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

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

Open 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, לבקר את התיעוד .

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;
    }
  }
}

הצהרת המשחק הראשונה תואמת את שמו subcollection 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, בקר במשאבים הבאים: