الدرس التطبيقي حول ترميز Cloud Firestore لنظام التشغيل iOS

1. نظرة عامة

الأهداف

في هذا الدرس التطبيقي حول الترميز، ستُنشئ تطبيقًا لاقتراح المطاعم يستند إلى Firestore على نظام التشغيل iOS باستخدام Swift. ستتعرّف على كيفية:

  1. قراءة البيانات وكتابتها في Firestore من تطبيق iOS
  2. الاستماع إلى التغييرات في بيانات Firestore في الوقت الفعلي
  3. استخدام قواعد الأمان ومصادقة Firebase لتأمين بيانات Firestore
  4. كتابة طلبات بحث معقدة في Firestore

المتطلبات الأساسية

قبل بدء هذا الدليل التعليمي حول الرموز البرمجية، تأكَّد من تثبيت ما يلي:

  • الإصدار 14.0 من Xcode (أو إصدار أحدث)
  • ‫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)

تُضيف الرمز البرمجي أعلاه مستندًا جديدًا إلى مجموعة المطاعم. تأتي بيانات المستند من قاموس نحصل عليه من بنية Restaurant.

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

في علامة التبويب "القواعد" في وحدة تحكّم Firebase، أضِف القواعد التالية ثم انقر على نشر.

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

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

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

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

Screen Shot 2017-07-06 at 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 وظيفة المعاملات التي تتيح لنا تنفيذ عمليات قراءة وكتابة متعددة في عملية اتّحادية واحدة، ما يضمن بقاء بياناتنا متسقة.

أضِف الرمز البرمجي التالي أسفل كل تعريفات 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 /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;
    }
  }
}

المتغيّر request في القواعد هو متغيّر عام متاح في جميع القواعد، ويضمن الشرط الذي أضفناه مصادقة الطلب قبل السماح للمستخدمين بتنفيذ أي إجراء. ويؤدي ذلك إلى منع المستخدمين غير المعتمَدين من استخدام واجهة برمجة التطبيقات Firestore API لإجراء تغييرات غير مصرّح بها على بياناتك. هذه بداية جيدة، ولكن يمكننا استخدام قواعد Firestore لتنفيذ إجراءات أكثر فعالية.

نريد تقييد عمليات كتابة المراجعات بحيث يجب أن يتطابق رقم تعريف المستخدم في المراجعة مع رقم تعريف المستخدم الذي تم مصادقة هويته. ويضمن ذلك عدم تمكّن المستخدمين من انتحال هوية بعضهم البعض ونشر مراجعات احتيالية.

يتطابق بيان المطابقة الأول مع المجموعة الفرعية التي تحمل الاسم 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، يُرجى الانتقال إلى المراجع التالية: