Cloud Firestore iOS Codelab

1. Khái quát chung

Bàn thắng

Trong phòng thí nghiệm lập trình 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 các quy tắc bảo mật và Xác thực Firebase để 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 chắc chắn rằng bạn đã cài đặt:

  • Phiên bản Xcode 14.0 (hoặc cao hơn)
  • CocoaPods 1.12.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. Lấy 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 sẽ biên dịch chính xác và ngay lập tức gặp sự cố khi khởi chạy vì ứng dụng thiếu tệp GoogleService-Info.plist . Chúng tôi sẽ sửa nó trong bước tiếp theo.

Thiết lập căn cứ hỏa lực

Làm theo tài liệu để tạo 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 tệp đó vào thư mục gốc của dự án Xcode. Chạy lại dự án để đảm bảo ứng dụng được đị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 phần 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ể đưa vào giao diện người dùng ứng dụng. Điều này có thể được thực hiện thủ công thông qua bảng điều khiển Firebase nhưng chúng tôi sẽ thực hiện việc này trong chính ứng dụng để thể hiện cách ghi 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 các 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ề 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 chiếu đến bộ sưu tập nhà hàng. Thêm phần sau vào vòng lặp for bên trong phương thức RestaurantsTableViewController.didTapPopulateButton(_:) .

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

Bây giờ chúng tôi có một tài liệu tham khảo bộ sưu tập, chúng tôi 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ừ một từ điển mà chúng tôi lấy từ cấu trúc Nhà hàng.

Chúng ta gần như hoàn thành – trước khi có thể ghi tài liệu vào Firestore, chúng ta cần mở các quy tắc bảo mật của Firestore và mô tả phần nào trong cơ sở dữ liệu của chúng ta mà người dùng nào có thể ghi được. Hiện tại, chúng tôi sẽ chỉ cho phép người dùng được 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 sự cố xác thực trong khi thử nghiệm. Ở phần cuối của phòng thí nghiệm lập trình 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ề 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 vào " ở phía trên bên trái, thao tác này sẽ tạo ra 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 vào Firestore từ một ứng dụng iOS! Trong phần tiếp theo, bạn sẽ tìm hiểu cách truy xuất dữ liệu từ Firestore và hiển thị dữ liệu đó trong ứng dụng.

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

Trong phần này, bạn sẽ tìm hiểu cách truy xuất dữ liệu từ Firestore và hiển thị dữ liệu đó trong ứng dụng. Hai bước chính là tạo truy vấn và thêm trình xử lý ả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ác bản cập nhật trong thời gian thực.

Trước 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 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 của 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 đính kèm một trình xử lý ảnh chụp nhanh để tải dữ liệu từ Firestore vào ứng dụng của chúng tôi. Thêm đoạn 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ộ. Cuộc gọi addSnapshotListener(_:) thêm một trình xử lý ả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á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 xử lý ảnh chụp nhanh này có thể được gọi bất cứ lúc nào do 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 ta tới các cấu trúc (xem Restaurant.swift ), việc hiển thị dữ liệu chỉ là vấn đề 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 đảm nhiệm việc ánh xạ tập hợp các loại giá trị từ trước tới các ô của chế độ 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 để kích hoạt tính năng 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")

Đúng như tên gọi của nó, phương thức whereField(_:isEqualTo:) sẽ làm cho truy vấn của chúng ta chỉ tải xuống các thành viên của bộ sưu tập có trường đáp ứng các hạn chế mà chúng ta đã đặt. 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 được đặ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 ta sẽ chỉ trả về những 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 tên danh mục và 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 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/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=...}

Điều này là do Firestore yêu cầu các chỉ mục cho hầu hết các truy vấn phức hợp. Việc yêu cầu lập chỉ mục cho các truy vấn giúp Firestore nhanh chóng mở rộng quy mô. Việc 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ố được điền chính xác. Để 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 bài viết của chúng tôi đều là nguyên tử và tương đối đơn giản. Nếu bất kỳ trong số chúng bị lỗi, chúng tôi có thể chỉ cần nhắc người dùng thử lại chúng hoặc tự động thử lại chúng.

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

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

Người dùng ứng dụng của chúng tôi sẽ 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 sẽ 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.

Trước tiên, hãy xem xét kỹ hơn các quy tắc bảo mật mà chúng tôi đã viết khi bắt đầu phòng thí nghiệm mã. 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à biến toàn cầu 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 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 của Firestore để làm những việc 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 được 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 so khớp đầu tiên khớp với bộ sưu tập con có tên ratings của bất kỳ tài liệu nào thuộc bộ sưu tập restaurants . Sau đó, điều kiện allow write sẽ ngăn mọi đánh giá được gửi 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 đối sánh thứ hai cho phép bất kỳ người dùng được xác thực nào đọ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 bài đá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 bài đá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á, chính bộ quy tắ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 của Firestore cũng có thể được sử dụng theo cách chi tiết hơn để giới hạn 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 ý 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 đã chia 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 thao tác nào sẽ được phép. Bất kỳ người dùng nào cũng có thể ghi nhà hàng vào cơ sở dữ liệu, giữ nguyên chức năng của nút Điền thông tin mà chúng tôi đã tạo khi bắt đầu phòng thí nghiệm mã, nhưng một khi nhà hàng được ghi 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 mọi hoạt động cập nhật 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 đã tồn tại 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 phòng thí nghiệm lập trình 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 nhánh codelab-complete .

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