Codelab של Cloud Firestore iOS

מטרות

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

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

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

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

  • גרסת 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

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

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

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

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

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

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

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

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

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

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

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

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

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