Cloud Firestore iOS Codelab

1. نظرة عامة

الأهداف

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

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

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

قبل البدء في هذا الكود ، تأكد من أنك قمت بتثبيت:

  • الإصدار 8.3 من Xcode (أو أعلى)
  • CocoaPods 1.2.1 (أو أعلى)

2. إنشاء مشروع وحدة تحكم Firebase

أضف Firebase إلى المشروع

  1. انتقل إلى وحدة التحكم Firebase .
  2. حدد إنشاء مشروع جديد واسم المشروع "Firestore دائرة الرقابة الداخلية Codelab".

3. احصل على عينة المشروع

قم بتنزيل الكود

تبدأ من خلال استنساخ نموذج المشروع وتشغيل pod update في دليل المشروع:

git clone https://github.com/firebase/friendlyeats-ios
cd friendlyeats-ios
pod update

مفتوحة FriendlyEats.xcworkspace في كسكودي وتشغيلها (كمد + 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 مساءً.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!

2ca7f8c6052f7f79.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/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 سريعًا على نطاق واسع. وفتح الارتباط من رسالة الخطأ تلقائيا فتح UI إنشاء فهرس في وحدة تحكم 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 ، قم بزيارة الموارد التالية: