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 ، نحتاج إلى الحصول على مرجع لمجموعة المطاعم. أضف ما يلي إلى حلقة for الداخلية في طريقة 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;
    }
  }
}

سنناقش قواعد الأمان بالتفصيل لاحقًا ، ولكن إذا كنت في عجلة من أمرك ، فألق نظرة على وثائق قواعد الأمان .

قم بتشغيل التطبيق وتسجيل الدخول. ثم انقر فوق الزر " Populate " في الجزء العلوي الأيسر ، والذي سينشئ مجموعة من مستندات المطعم ، على الرغم من أنك لن ترى هذا في التطبيق حتى الآن.

بعد ذلك ، انتقل إلى علامة تبويب بيانات Firestore في وحدة تحكم Firebase. يجب أن تشاهد الآن إدخالات جديدة في مجموعة المطاعم:

لقطة شاشة 2017-07-06 الساعة 12.45.38 مساءً.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 المتقدم لتمكين التصفية.

فيما يلي مثال على استعلام بسيط لجلب جميع مطاعم Dim Sum:

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 وانتقل إلى قاعدة البيانات> القواعد في علامة التبويب 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;
    }
  }
}

هنا قمنا بتقسيم إذن الكتابة الخاص بنا إلى إنشاء وتحديث حتى نتمكن من أن نكون أكثر تحديدًا بشأن العمليات التي يجب السماح بها. يمكن لأي مستخدم كتابة مطاعم في قاعدة البيانات ، مع الاحتفاظ بوظيفة الزر "ملء" الذي أنشأناه في بداية مختبر الرموز ، ولكن بمجرد كتابة مطعم لا يمكن تغيير اسمه وموقعه وسعره وفئته. وبشكل أكثر تحديدًا ، تتطلب القاعدة الأخيرة أي عملية تحديث للمطعم للحفاظ على نفس الاسم والمدينة والسعر والفئة للحقول الموجودة بالفعل في قاعدة البيانات.

لمعرفة المزيد حول ما يمكنك فعله بقواعد الأمان ، ألق نظرة على الوثائق .

9. الخلاصة

في مختبر الرموز هذا ، تعلمت كيفية القراءة والكتابة الأساسية والمتقدمة باستخدام Firestore ، وكذلك كيفية تأمين الوصول إلى البيانات باستخدام قواعد الأمان. يمكنك العثور على الحل الكامل في الفرع codelab-complete .

لمعرفة المزيد حول Firestore ، قم بزيارة الموارد التالية: