Lớp học lập trình iOS của Cloud Firestore

1. Khái quát chung

Bàn thắng

Trong lớp học lập trình này, bạn sẽ xây dựng ứng dụng đề xuất nhà hàng do 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 lớp học lập trình này, hãy đảm bảo bạn đã cài đặt:

  • Xcode phiên bản 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 phải 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 này thiếu tệp GoogleService-Info.plist . Chúng tôi sẽ sửa lỗi đó 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 nó 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 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 để có thể đưa vào giao diện người dùng ứng dụng. Việc 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 đó trong chính ứng dụng để minh họa cách viết cơ bản trên Firestore.

Đố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 trong phương thức RestaurantsTableViewController.didTapPopulateButton(_:) .

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

Bây giờ chúng ta có một tài liệu tham khảo về bộ sưu tập, chúng ta có thể viết một số dữ liệu. Thêm dòng 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 nhận được từ cấu trúc Nhà hàng.

Chúng ta gần như đã hoàn tất—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ả những 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 thứ gì đó đủ thoải mái để không liên tục gặp phải các vấn đề xác thực trong khi thử nghiệm. Ở cuối lớp học lập trình này, chúng ta sẽ nói về cách thắt chặt các quy tắc bảo mật 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 đó chạm vào nút " Populate " ở phía trên bên trái, thao tác này sẽ tạo một loạt tài liệu về 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ừ ứ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 nghe ảnh chụp nhanh. Listener 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ập nhật theo 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 cách triển khai RestaurantsTableViewController.baseQuery() :

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

Truy vấn này truy xuất tới 50 nhà hàng trong bộ sưu tập cấp cao nhất có tên là "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 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 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 gọi 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ập nhật tự động và không phải thực hiện 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 kỳ 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 xem bảng, đảm nhiệm việc ánh xạ tập hợp các loại giá trị từ trước đến 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 phần này thành công, ứng dụng của bạn hiện đang đọc và ghi dữ liệu bằng 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 để bật tính năng lọc.

Dưới đây là ví dụ về 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ẽ khiến 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ó các trường đáp ứng các giới hạn mà chúng ta đặt ra. Trong trường hợp này, nó sẽ chỉ tải xuống những 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ể, 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 đề thứ tự để xây dựng một truy vấn phức hợp duy nhất dựa trên thông tin đầ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ề 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 lập chỉ mục cho hầu hết các truy vấn phức hợp. Yêu cầu lập chỉ mục trên các truy vấn giúp Firestore có tốc độ nhanh trên quy mô lớn. 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ề các chỉ mục trong Firestore, hãy truy cập tài liệu .

7. Ghi dữ liệu trong 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 nhà hàng. Cho đến nay, tất cả các bài viết của chúng tôi đều rất đơn giản và tương đối đơn giản. Nếu bất kỳ lỗi nào trong số đó bị lỗi, chúng tôi có thể chỉ nhắc người dùng thử lại hoặc tự động thử lại.

Để 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. Đầu 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 những điều này không thành công nhưng cái kia thì không, thì chúng ta sẽ rơi vào 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 ta 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 mã sau đây 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 trên máy chủ không thành công, Firestore sẽ tự động thử lại 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ố 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 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 đề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 ở phần phụ trợ để được bảo mật hoàn 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 cùng tìm hiểu sâu hơn về các quy tắc bảo mật mà chúng tôi đã viết khi bắt đầu lớp học lập trình. 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ụ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 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ệc ghi đánh giá để ID người dùng của đá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 lẫn nhau và để lại những đá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;
    }
  }
}

Tuyên bố so khớp đầu tiên khớp với tập hợ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 . allow write có điều kiện sau đó sẽ ngăn không cho gửi bất kỳ bài đánh giá nào nếu ID người dùng của bài đá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 được xác thực nào đọc và ghi 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õ lời đả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á thì bộ quy tắc tương tự 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 để 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 địa điểm 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 thành tạo và cập nhật để 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, duy trì chức năng của nút Điền mà chúng tôi đã tạo khi bắt đầu lớp học lập trình, nhưng sau khi ghi tên nhà hàng, địa điểm, giá và danh mục thì không thể thay đổi. 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á cả 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 lớp học lập trình này, bạn đã tìm hiểu cách đọc và ghi cơ bản và nâng cao bằng 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: