Cloud Firestore iOS Codelab

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

1. Sơ lượt

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 13.0 (hoặc cao hơn)
  • CocoaPods 1.11.0 (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. Chuyển đến bảng điều khiển Firebase .
  2. Chọn Tạo dự án mới và đặt tên cho dự án của bạn là "Firestore iOS Codelab".

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

Tải xuống mã

Bắt đầu bằng cách sao chép 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 phải biên dịch chính xác và gặp sự cố ngay lập tức khi khởi chạy, vì nó thiếu tệp GoogleService-Info.plist . Chúng tôi sẽ sửa điều đó trong bước tiếp theo.

Thiết lập Firebase

Làm theo tài liệu để tạo một dự án Firestore mới. Khi bạn đã có dự án của mình, hãy tải xuống tệp GoogleService-Info.plist của dự án từ bảng điều khiển Firebase 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.

d5225270159c040b.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 theo cách thủ công thông qua bảng điều khiển Firebase , nhưng chúng tôi sẽ thực hiện nó trong chính ứng dụng để chứng minh cách viết Firestore 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 dưới dạng tài liệu trong 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, hãy đọc về các tài liệu và 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 phần sau vào vòng lặp for bên trong trong phương thức RestaurantsTableViewController.didTapPopulateButton(_:) .

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 lên 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 trong 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 Quy tắc của bảng điều khiển Firebase, hãy thêm các quy tắc sau rồi 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 ta sẽ thảo luận chi tiết về các quy tắc bảo mật sau, nhưng nếu bạn đang vội, hãy xem tài liệu về các quy tắc bảo mật .

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

Tiếp theo, điều hướng đến tab dữ liệu Firestore trong bảng điều khiển Firebase. 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ó phù hợ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 cách triển khai của 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 tôi có một truy vấn, chúng tôi cần phải đí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 tôi. Thêm mã sau vào phương thức RestaurantsTableViewController.observeQuery() ngay sau lệnh gọi 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ộ. Lệnh addSnapshotListener(_:) thêm trình nghe ảnh chụp nhanh vào truy vấn sẽ cập nhật bộ điều khiển chế độ 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 ánh xạ từ điển của chúng tôi với cấu trúc (xem Restaurant.swift ), việc hiển thị dữ liệu chỉ là việc gán một vài thuộc tính chế độ xem. Thêm các dòng sau vào 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 thức điền này được gọi từ phương thức tableView(_:cellForRowAtIndexPath:) của nguồn dữ liệu chế độ xem bảng, phương thức này sẽ xử lý ánh xạ tập hợp các loại giá trị từ trước tới các ô xem bảng riêng lẻ.

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 thành công phần này, ứng dụng của bạn hiện đang đọc và ghi dữ liệu với Cloud Firestore!

391c0259bf05ac25.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à 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ó, phương whereField(_:isEqualTo:) sẽ làm cho truy vấn của chúng tôi chỉ tải xuống các thành viên của bộ sưu tập có các trường đáp ứng các hạn chế mà chúng tôi đặt ra. Trong trường hợp này, nó sẽ chỉ tải xuống các nhà hàng có 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 mã ở trên thêm nhiều mệnh đề whereFieldorder để tạo một truy vấn phức hợp duy nhất dựa trên đầu vào của người dùng. 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ừ thông báo lỗi sẽ tự động mở giao diện người dùng tạo chỉ mục trong bảng điều khiển Firebase với các thông số chính xác được điền vào. Để tìm hiểu thêm về chỉ mục trong Firestore, hãy truy cập 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, trong đó 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 hoạt động 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 vào bên dưới tất cả các khai báo 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ở bảng điều khiển Firebase và điều hướng đến Cơ sở dữ liệu> Quy tắc 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;
    }
  }
}

Biến request 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à điều kiện mà chúng tôi đã thêm đảm bảo rằng yêu cầu được xác thực trước khi cho phép người dùng thực hiện bất kỳ điều gì. Điều này ngăn người dùng chưa được xác thực sử dụng Firestore API để 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;
    }
  }
}

Câu lệnh đối sánh đầu tiên khớp với ratings của bộ sưu tập con có tên của bất kỳ tài liệu nào thuộc bộ sưu tập restaurants . Sau đó, allow write có điều kiện ngăn không cho gửi bất kỳ đánh giá nào nếu ID người dùng của đánh giá không khớp với ID 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ề 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 bảo mật, hãy xem tài liệu .

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 thấy giải pháp đầy đủ trên codelab-complete .

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