Cloud Firestore iOS Codelab

1. Tổng quan

Bàn thắng

Trong bảng mã này, bạn sẽ xây dựng ứng dụng đề xuất nhà hàng được Firestore hỗ trợ trên iOS bằng Swift. Bạn sẽ học cách:

  1. Đọc và ghi dữ liệu vào Firestore từ ứng dụng iOS
  2. Lắng nghe những thay đổi trong dữ liệu Firestore trong thời gian thực
  3. Sử dụng Xác thực Firebase và các quy tắc bảo mật để bảo mật dữ liệu Firestore
  4. Viết các truy vấn Firestore phức tạp

Điều kiện tiên quyết

Trước khi bắt đầu codelab này, hãy đảm bảo rằng bạn đã cài đặt:

  • Xcode phiên bản 8.3 (hoặc cao hơn)
  • CocoaPods 1.2.1 (hoặc cao hơn)

2. Tạo dự án bảng điều khiển Firebase

Thêm Firebase vào dự án

  1. Tới các căn cứ hỏa lực console .
  2. Chọn Tạo dự án mới và đặt tên cho dự án của bạn "FireStore iOS Codelab".

3. Nhận dự án mẫu

Tải xuống mã

Bắt đầu bằng cách nhân bản dự án mẫu và chạy pod update trong thư mục dự án:

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

Mở FriendlyEats.xcworkspace trong Xcode và chạy nó (Cmd + R). Ứng dụng nên biên dịch một cách chính xác và ngay lập tức sụp đổ vào mắt, vì nó thiếu một GoogleService-Info.plist tập tin. Chúng tôi sẽ sửa điều đó trong bước tiếp theo.

Thiết lập Firebase

Thực hiện theo các tài liệu hướng dẫn để tạo ra một dự án FireStore mới. Một khi bạn đã có dự án của bạn, tải về dự án của bạn GoogleService-Info.plist tập tin từ căn cứ hỏa lực console và kéo nó vào thư mục gốc của dự án Xcode. Chạy lại dự án để đảm bảo rằng ứng dụng định cấu hình chính xác và không còn gặp sự cố khi khởi chạy. Sau khi đăng nhập, bạn sẽ thấy một màn hình trống như ví dụ bên dưới. Nếu bạn không thể đăng nhập, hãy đảm bảo rằng bạn đã bật phương thức đăng nhập Email / Mật khẩu trong bảng điều khiển Firebase trong Xác thực.

10a0671ce8f99704.png

4. Ghi dữ liệu vào Firestore

Trong phần này, chúng tôi sẽ ghi một số dữ liệu vào Firestore để chúng tôi có thể điền vào giao diện người dùng ứng dụng. Điều này có thể được thực hiện bằng tay thông qua các căn cứ hỏa lực console , nhưng chúng tôi sẽ làm điều đó trong ứng dụng riêng của mình để chứng minh một FireStore ghi cơ bản.

Đối tượng mô hình chính trong ứng dụng của chúng tôi là một nhà hàng. Dữ liệu Firestore được chia thành tài liệu, bộ sưu tập và bộ sưu tập con. Chúng tôi sẽ lưu trữ mỗi nhà hàng như một tài liệu trong một bộ sưu tập cấp cao nhất được gọi là restaurants . Nếu bạn muốn tìm hiểu thêm về mô hình dữ liệu FireStore, đọc về các tài liệu và các bộ sưu tập trong tài liệu .

Trước khi có thể thêm dữ liệu vào Firestore, chúng tôi cần tham khảo bộ sưu tập nhà hàng. Thêm sau vào bên trong vòng lặp for trong RestaurantsTableViewController.didTapPopulateButton(_:) phương pháp.

let collection = Firestore.firestore().collection("restaurants")

Bây giờ chúng ta có một tham chiếu bộ sưu tập, chúng ta có thể viết một số dữ liệu. Thêm phần sau ngay sau dòng mã cuối cùng mà chúng tôi đã thêm:

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)

Đoạn mã trên thêm một tài liệu mới vào bộ sưu tập nhà hàng. Dữ liệu tài liệu đến từ từ điển, chúng tôi lấy từ cấu trúc Nhà hàng.

Chúng ta sắp hoàn thành rồi – trước khi có thể viết tài liệu cho Firestore, chúng ta cần mở các quy tắc bảo mật của Firestore và mô tả những phần nào của cơ sở dữ liệu của chúng ta mà người dùng có thể ghi được. Hiện tại, chúng tôi sẽ chỉ cho phép những người dùng đã xác thực đọc và ghi vào toàn bộ cơ sở dữ liệu. Điều này hơi quá dễ dãi đối với một ứng dụng sản xuất, nhưng trong quá trình xây dựng ứng dụng, chúng tôi muốn một cái gì đó đủ thoải mái để chúng tôi không liên tục gặp phải các vấn đề xác thực trong khi thử nghiệm. Ở phần cuối của codelab này, chúng ta sẽ nói về cách củng cố các quy tắc bảo mật của bạn và hạn chế khả năng đọc và ghi ngoài ý muốn.

Trong tab Rules của các firebase console thêm các quy tắc sau đây và sau đó nhấp vào Xuất bản.

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

Chúng tôi sẽ thảo luận về quy tắc an ninh chi tiết sau, nhưng nếu bạn đang ở trong một vội vàng, hãy nhìn vào các tài liệu hướng dẫn quy tắc bảo mật .

Chạy ứng dụng và đăng nhập. Sau đó bấm vào nút "cư" ở phía trên bên trái, mà sẽ tạo ra một loạt các văn bản nhà hàng, mặc dù bạn sẽ không thấy điều này trong ứng dụng được nêu ra.

Tiếp theo, điều hướng đến tab dữ liệu FireStore trong căn cứ hỏa lực console. Bây giờ bạn sẽ thấy các mục mới trong bộ sưu tập nhà hàng:

Ảnh chụp màn hình 2017-07-06 lúc 12.45.38 PM.png

Xin chúc mừng, bạn vừa ghi dữ liệu lên Firestore từ một ứng dụng iOS! Trong phần tiếp theo, bạn sẽ tìm hiểu cách lấy dữ liệu từ Firestore và hiển thị nó trong ứng dụng.

5. Hiển thị dữ liệu từ Firestore

Trong phần này, bạn sẽ học cách lấy dữ liệu từ Firestore và hiển thị nó trong ứng dụng. Hai bước quan trọng là tạo truy vấn và thêm trình nghe ảnh chụp nhanh. Người nghe này sẽ được thông báo về tất cả dữ liệu hiện có khớp với truy vấn và nhận cập nhật trong thời gian thực.

Đầu tiên, hãy xây dựng truy vấn sẽ phục vụ danh sách nhà hàng mặc định, chưa được lọc. Hãy xem việc thực hiện RestaurantsTableViewController.baseQuery() :

return Firestore.firestore().collection("restaurants").limit(to: 50)

Truy vấn này truy xuất tối đa 50 nhà hàng thuộc bộ sưu tập cấp cao nhất có tên "nhà hàng". Bây giờ chúng ta có một truy vấn, chúng ta cần đính kèm một trình nghe ảnh chụp nhanh để tải dữ liệu từ Firestore vào ứng dụng của chúng ta. Thêm mã sau vào RestaurantsTableViewController.observeQuery() phương pháp chỉ sau khi cuộc gọi đến 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()
}

Đoạn mã trên tải xuống bộ sưu tập từ Firestore và lưu trữ nó trong một mảng cục bộ. Các addSnapshotListener(_:) gọi thêm một người biết lắng nghe chụp với truy vấn mà sẽ cập nhật bộ điều khiển xem mỗi khi dữ liệu thay đổi trên máy chủ. Chúng tôi nhận các bản cập nhật tự động và không phải đẩy các thay đổi theo cách thủ công. Hãy nhớ rằng, trình nghe ảnh chụp nhanh này có thể được gọi bất kỳ lúc nào do kết quả của sự thay đổi phía máy chủ, vì vậy điều quan trọng là ứng dụng của chúng tôi có thể xử lý các thay đổi.

Sau khi lập bản đồ từ điển của chúng tôi để cấu trúc (xem Restaurant.swift ), hiển thị dữ liệu là chỉ là vấn đề gán một vài thuộc tính xem. Thêm các dòng sau để RestaurantTableViewCell.populate(restaurant:) trong 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)

Phương pháp populate này được gọi là từ nguồn dữ liệu của xem bảng tableView(_:cellForRowAtIndexPath:) phương pháp, mà sẽ chăm sóc của việc lập bản đồ bộ sưu tập các kiểu giá trị từ trước đến các tế bào xem bảng cá nhân.

Chạy lại ứng dụng và xác minh rằng các nhà hàng mà chúng ta đã thấy trước đó trong bảng điều khiển hiện đã hiển thị trên trình mô phỏng hoặc thiết bị. Nếu bạn hoàn thành phần này thành công, ứng dụng của bạn hiện đang đọc và ghi dữ liệu với Cloud Firestore!

2ca7f8c6052f7f79.png

6. Sắp xếp và lọc dữ liệu

Hiện tại, ứng dụng của chúng tôi hiển thị danh sách các nhà hàng, nhưng không có cách nào để người dùng lọc dựa trên nhu cầu của họ. Trong phần này, bạn sẽ sử dụng truy vấn nâng cao của Firestore để cho phép lọc.

Dưới đây là một ví dụ về một truy vấn đơn giản để tìm nạp tất cả các nhà hàng Dim Sum:

let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")

Như tên gọi của nó ngụ ý, các whereField(_:isEqualTo:) phương pháp sẽ làm cho tải về truy vấn của chúng tôi chỉ thành viên của tập hợp có nghề đạt những hạn chế, chúng tôi thiết lập. Trong trường hợp này, nó sẽ chỉ tải nhà hàng, nơi category"Dim Sum" .

Trong ứng dụng này, người dùng có thể xâu chuỗi nhiều bộ lọc để tạo các truy vấn cụ thể, chẳng hạn như "Pizza ở San Francisco" hoặc "Hải sản ở Los Angeles đặt hàng theo mức độ phổ biến".

Mở RestaurantsTableViewController.swift và thêm khối mã sau vào giữa 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)
}

Đoạn trên cho biết thêm nhiều whereFieldorder các điều khoản để xây dựng một truy vấn hợp chất duy nhất dựa trên người dùng nhập vào. Bây giờ truy vấn của chúng tôi sẽ chỉ trả về các nhà hàng phù hợp với yêu cầu của người dùng.

Chạy dự án của bạn và xác minh rằng bạn có thể lọc theo giá, thành phố và danh mục (đảm bảo nhập chính xác danh mục và tên thành phố). Trong khi kiểm tra, bạn có thể thấy các lỗi trong nhật ký của mình trông giống như sau:

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=...}

Điều này là do Firestore yêu cầu chỉ mục cho hầu hết các truy vấn phức hợp. Yêu cầu chỉ mục trên các truy vấn giúp Firestore nhanh chóng trên quy mô. Mở liên kết từ các thông báo lỗi sẽ tự động mở giao diện người dùng sáng tạo chỉ mục trong các căn cứ hỏa lực console với các thông số đúng điền vào. Để tìm hiểu thêm về các chỉ số trong FireStore, tham quan các tài liệu .

7. Ghi dữ liệu trong một giao dịch

Trong phần này, chúng tôi sẽ thêm khả năng để người dùng gửi đánh giá cho các nhà hàng. Cho đến nay, tất cả các chữ viết của chúng tôi đều là nguyên tử và tương đối đơn giản. Nếu có bất kỳ lỗi nào trong số chúng bị lỗi, chúng tôi có thể chỉ nhắc người dùng thử lại chúng hoặc tự động thử lại.

Để thêm xếp hạng cho một nhà hàng, chúng ta cần phối hợp nhiều lượt đọc và ghi. Đầu tiên bản đánh giá phải được gửi, sau đó cần cập nhật số lượng xếp hạng và xếp hạng trung bình của nhà hàng. Nếu một trong hai lỗi này không thành công mà không phải lỗi kia, chúng ta sẽ ở trong trạng thái không nhất quán, nơi dữ liệu trong một phần cơ sở dữ liệu của chúng tôi không khớp với dữ liệu trong một phần khác.

May mắn thay, Firestore cung cấp chức năng giao dịch cho phép chúng tôi thực hiện nhiều lần đọc và ghi trong một thao tác nguyên tử duy nhất, đảm bảo rằng dữ liệu của chúng tôi vẫn nhất quán.

Thêm mã sau đây tất cả các tờ khai let trong 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)
    }
  }
}

Bên trong khối cập nhật, tất cả các hoạt động chúng tôi thực hiện bằng cách sử dụng đối tượng giao dịch sẽ được Firestore coi là một bản cập nhật nguyên tử duy nhất. Nếu cập nhật không thành công trên máy chủ, Firestore sẽ tự động thử lại một vài lần. Điều này có nghĩa là tình trạng lỗi của chúng tôi rất có thể là một lỗi duy nhất xảy ra lặp đi lặp lại, ví dụ: nếu thiết bị hoàn toàn ngoại tuyến hoặc người dùng không được phép ghi vào đường dẫn mà họ đang cố gắng ghi vào.

8. Quy tắc bảo mật

Người dùng ứng dụng của chúng tôi không thể đọc và ghi mọi phần dữ liệu trong cơ sở dữ liệu của chúng tôi. Ví dụ: mọi người đều có thể xem xếp hạng của nhà hàng, nhưng chỉ người dùng đã được xác thực mới được phép đăng xếp hạng. Viết mã tốt trên máy khách là chưa đủ, chúng ta cần chỉ định mô hình bảo mật dữ liệu của mình trên phần phụ trợ để hoàn toàn an toàn. Trong phần này, chúng ta sẽ tìm hiểu cách sử dụng các quy tắc bảo mật của Firebase để bảo vệ dữ liệu của mình.

Đầu tiên, chúng ta hãy xem xét sâu hơn các quy tắc bảo mật mà chúng tôi đã viết ở phần đầu của codelab. Mở căn cứ hỏa lực console và điều hướng đến Cơ sở dữ liệu> Rules trong tab 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;
    }
  }
}

Các request biến trong các quy tắc trên là một biến toàn cục có sẵn trong tất cả các quy tắc, và chúng ta có điều kiện bổ sung đảm bảo rằng các yêu cầu được xác thực trước khi cho phép người sử dụng để làm gì cả. Điều này ngăn người dùng chưa được xác thực sử dụng API Firestore để thực hiện các thay đổi trái phép đối với dữ liệu của bạn. Đây là một khởi đầu tốt, nhưng chúng ta có thể sử dụng các quy tắc Firestore để làm những điều mạnh mẽ hơn nhiều.

Hãy hạn chế viết bài đánh giá để ID người dùng của bài đánh giá phải khớp với ID của người dùng đã xác thực. Điều này đảm bảo rằng người dùng không thể mạo danh nhau và để lại các đánh giá gian lận. Thay thế các quy tắc bảo mật của bạn bằng các quy tắc sau:

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

Báo cáo kết quả trận đấu đầu tiên phù hợp với subcollection tên ratings của bất kỳ tài liệu thuộc restaurants bộ sưu tập. Các allow write điều kiện sau đó ngăn chặn bất kỳ xem xét từ được gửi nếu ID người dùng của các nghiên cứu không phù hợp đó của người dùng. Câu lệnh so khớp thứ hai cho phép bất kỳ người dùng đã xác thực nào có thể đọc và ghi các nhà hàng vào cơ sở dữ liệu.

Điều này thực sự hiệu quả đối với các đánh giá của chúng tôi, vì chúng tôi đã sử dụng các quy tắc bảo mật để nêu rõ ràng đảm bảo ngầm mà chúng tôi đã viết vào ứng dụng của mình trước đó – rằng người dùng chỉ có thể viết đánh giá của riêng họ. Nếu chúng tôi thêm chức năng chỉnh sửa hoặc xóa cho các bài đánh giá, thì bộ quy tắc chính xác này cũng sẽ ngăn người dùng sửa đổi hoặc xóa các bài đánh giá của người dùng khác. Nhưng các quy tắc Firestore cũng có thể được sử dụng theo cách chi tiết hơn để hạn chế việc ghi trên các trường riêng lẻ trong tài liệu thay vì toàn bộ tài liệu. Chúng tôi có thể sử dụng điều này để cho phép người dùng chỉ cập nhật xếp hạng, xếp hạng trung bình và số lượng xếp hạng cho một nhà hàng, loại bỏ khả năng người dùng độc hại thay đổi tên hoặc vị trí nhà hàng.

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

Ở đây, chúng tôi đã tách quyền ghi của mình thành tạo và cập nhật để chúng tôi có thể cụ thể hơn về những hoạt động nào nên được phép. Bất kỳ người dùng nào cũng có thể ghi các nhà hàng vào cơ sở dữ liệu, giữ nguyên chức năng của nút Populate mà chúng tôi đã thực hiện ở đầu bảng mã, nhưng một khi nhà hàng được viết tên, vị trí, giá cả và danh mục thì không thể thay đổi được. Cụ thể hơn, quy tắc cuối cùng yêu cầu bất kỳ hoạt động cập nhật nào của nhà hàng phải duy trì cùng tên, thành phố, giá và danh mục của các trường đã có trong cơ sở dữ liệu.

Để tìm hiểu thêm về những gì bạn có thể làm với các quy tắc an ninh, hãy nhìn vào các tài liệu hướng dẫn .

9. Kết luận

Trong bảng mã này, bạn đã học cách đọc và ghi cơ bản và nâng cao với Firestore, cũng như cách bảo mật quyền truy cập dữ liệu bằng các quy tắc bảo mật. Bạn có thể tìm ra giải pháp đầy đủ trên codelab-complete chi nhánh .

Để tìm hiểu thêm về Firestore, hãy truy cập các tài nguyên sau: