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، نحتاج إلى الحصول على مرجع لمجموعة المطاعم. قم بإضافة ما يلي إلى الحلقة الداخلية في أسلوب 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 الساعة 38.45.12 مساءً.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 وظيفة المعاملات التي تتيح لنا إجراء عمليات قراءة وكتابة متعددة في عملية ذرية واحدة، مما يضمن بقاء بياناتنا متسقة.

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