Lớp học lập trình iOS trên Cloud Firestore

1. Tổng quan

Bàn thắng

Trong lớp học lập trình này, bạn sẽ dùng Swift để xây dựng một ứng dụng đề xuất nhà hàng dựa trên Firestore dành cho iOS. Bạn sẽ tìm hiểu cách:

  1. Đọc và ghi dữ liệu vào Firestore từ một ứng dụng iOS
  2. Theo dõi các thay đổi trong dữ liệu Firestore theo 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 phức tạp trên Firestore

Đ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 (trở lên)
  • CocoaPods 1.12.0 (trở lên)

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

Thêm Firebase vào dự án

  1. Truy cập vào bảng điều khiển của Firebase.
  2. Chọn Create New Project (Tạo dự án mới) rồi đặt tên cho dự án là "Firestore iOS Codelab".

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

Tải mã nguồn xuống

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 rồi chạy (Cmd+R). Ứng dụng phải biên dịch đúng cách 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ẽ khắc phục vấn đề đó trong bước tiếp theo.

Thiết lập Firebase

Làm theo tài liệu này để tạo một dự án Firestore mới. Sau khi bạn đã có dự án, hãy tải tệp GoogleService-Info.plist của dự án xuống từ bảng điều khiển của Firebase rồi 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ụ dưới đây. Nếu không thể đăng nhập, hãy đảm bảo bạn đã bật phương thức đăng nhập bằng email/mật khẩu trong bảng điều khiển của 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 ta sẽ ghi một số dữ liệu vào Firestore để có thể điền sẵn vào giao diện người dùng của ứng dụng. Bạn có thể thực hiện việc này theo cách thủ công thông qua bảng điều khiển của Firebase, nhưng chúng ta sẽ thực hiện trong chính ứng dụng để minh hoạ cách ghi Firestore cơ bản.

Đối tượng mô hình chính trong ứng dụng của chúng ta là một nhà hàng. Dữ liệu trên Firestore được chia thành các tài liệu, bộ sưu tập và tập hợp con. Chúng ta sẽ lưu trữ mỗi nhà hàng dưới dạng tài liệu trong tập hợp cấp cao nhất có tên 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 các tài liệu và tập hợp trong tài liệu này.

Trước khi có thể thêm dữ liệu vào Firestore, chúng ta cần tham chiếu đến tập hợp nhà hàng. Thêm đoạn mã 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ó tham chiếu bộ sưu tập, chúng ta có thể ghi một số dữ liệu. Thêm đoạn mã sau ngay sau dòng mã cuối cùng mà chúng ta đã 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)

Mã ở trên thêm một tài liệu mới vào tập hợp nhà hàng. Dữ liệu tài liệu được lấy 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ả phần nào của cơ sở dữ liệu mà người dùng có thể ghi. Hiện tại, chúng tôi sẽ chỉ cho phép 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á thoải mái đối với ứng dụng chính thức, nhưng trong quá trình xây dựng ứng dụng, chúng ta muốn một điều gì đó đủ thoải mái để không liên tục gặp phải vấn đề về 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 củng cố các quy tắc bảo mật và hạn chế khả năng đọc và ghi ngoài ý muốn.

Trong thẻ Quy tắc của bảng điều khiển của 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 không thích, hãy xem tài liệu về quy tắc bảo mật.

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

Tiếp theo, hãy chuyển đến thẻFirestore data trong bảng điều khiển của Firebase. Lúc này, bạn sẽ thấy các mục mới trong tập hợp nhà hàng:

Ảnh chụp màn hình lúc 12.45.38 chiều 6/7/2017.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 nghe tổng quan nhanh. Trình 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 thông tin cập nhật theo thời gian thực.

Trước tiên, hãy tạo truy vấn sẽ phân phát danh sách các 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 đa 50 nhà hàng trong tập hợp cấp cao nhất có tên là "nhà hàng". Bây giờ, khi đã có truy vấn, chúng ta cần đính kèm một trình nghe tổng quan nhanh để tải dữ liệu từ Firestore vào ứng dụng. Thêm mã sau vào phương thức RestaurantsTableViewController.observeQuery() ngay sau lệnh 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()
}

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

Sau khi ánh xạ từ điển của chúng ta đến các cấu trúc (xem Restaurant.swift), việc hiển thị dữ liệu chỉ là việc chỉ định một số thuộc tính của thành phần hiển thị. 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 sẵn này được gọi từ phương thức tableView(_:cellForRowAtIndexPath:) của nguồn dữ liệu của chế độ xem theo bảng. Phương thức này đảm nhận việc liên kết tập hợp các loại giá trị từ trước đến từng ô trong chế độ xem bảng.

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 tất thành công phần này, thì giờ đây ứng dụng của bạn đã đọc và ghi dữ liệu bằng Cloud Firestore!

391c0259bf05ac25.pngS

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

Hiện tại, ứng dụng của chúng ta hiển thị danh sách các nhà hàng, nhưng người dùng chưa có cách nào để lọc theo nhu cầu. Trong phần này, bạn sẽ sử dụng tính nă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ề một truy vấn đơn giản để tìm nạp tất cả nhà hàng Dim Sum:

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

Như chí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 các thành viên của tập hợp có các trường đáp ứng các quy định hạn chế mà chúng ta đặt ra. Trong trường hợp này, Chrome sẽ chỉ tải những nhà hàng có category"Dim Sum" xuống.

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

Mở RestaurantsTableViewController.swift và thêm khối mã dưới đây 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 hoạt động đầu vào của người dùng. Hiện tại, truy vấn của chúng ta 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 nhận rằng bạn có thể lọc theo giá, thành phố và danh mục (hãy nhớ 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ý 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 chỉ mục cho hầu hết các truy vấn phức hợp. Việc yêu cầu chỉ mục đối với truy vấn giúp Firestore nhanh chóng mở rộng quy mô. Việc mở đường liên kết trong 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 của Firebase với các thông số chính xác được điền. Để tìm hiểu thêm về các chỉ mục trong Firestore, hãy tham khảo tài liệu này.

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

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

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

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

Thêm mã sau bên dưới mọi phần khai báo allow 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 thao tác mà chúng ta thực hiện bằng đối tượng giao dịch sẽ được Firestore coi là một lần cập nhật nguyên tử. Nếu máy chủ không cập nhật được, 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 ta rất có thể là một lỗi xảy ra lặp đi lặp lại, chẳng hạn như nếu thiết bị hoàn toàn không kết nối mạng 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 không nên đọc và ghi mọi phần dữ liệu trong cơ sở dữ liệu của chúng ta. Ví dụ: mọi người đều có thể nhìn thấy điểm xếp hạng của nhà hàng, nhưng chỉ người dùng đã xác thực mới được phép đăng điểm 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 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.

Trước tiên, hãy tìm hiểu kỹ hơn về các quy tắc bảo mật mà chúng ta đã viết ở đầu lớp học lập trình này. Mở bảng điều khiển của Firebase rồi chuyển đến Cơ sở dữ liệu > Quy tắc trong thẻ 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ó trong tất cả các quy tắc và biến có điều kiện mà chúng ta thêm vào giúp đảm bảo rằng yêu cầu được xác thực trước khi cho phép người dùng làm bất cứ điều gì. Điều này ngăn người dùng chưa 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 của Firestore để làm nhiều việc hiệu quả hơn.

Hãy hạn chế việc viết bài đánh giá để mã nhận dạng người dùng của bài đánh giá phải khớp với mã nhận dạng của người dùng đã được xác thực. Việc này giúp đảm bảo rằng người dùng không thể mạo danh nhau và để lại các bài đánh giá gian lận. Thay thế các quy tắc bảo mật bằng 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 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 tập hợp restaurants. Khi đó, điều kiện allow write sẽ ngăn việc gửi bài đánh giá nếu mã nhận dạng người dùng của bài đánh giá không khớp với mã nhận dạng người dùng của người dùng đó. Câu lệnh so khớp thứ hai cho phép mọi người dùng đã xác thực đọ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 ta, vì chúng ta đã sử dụng các quy tắc bảo mật để nêu rõ sự đảm bảo ngầm ẩn mà chúng ta đã viết vào ứng dụng 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 ta thêm chức năng chỉnh sửa hoặc xoá cho các bài đánh giá, thì cùng một bộ quy tắc này cũng sẽ ngăn người dùng sửa đổi hoặc xoá tên của những người dùng khác. bài đánh giá. Tuy nhiên, các quy tắc Firestore cũng có thể được sử dụng ở mức độ 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ì cho toàn bộ tài liệu. Chúng tôi có thể sử dụng chế độ này để cho phép người dùng chỉ cập nhật điểm xếp hạng, điểm xếp hạng trung bình và số lượng điểm xếp hạng cho một nhà hàng, qua đó loại bỏ khả năng người dùng cố ý chỉnh sửa tên hoặc vị trí của 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 ta đã 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 đượ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 mà vẫn duy trì chức năng của nút "Điền" mà chúng ta đã tạo ở đầu lớp học lập trình này. Tuy nhiên, sau khi nhà hàng viết tên, vị trí, giá 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 thao tác cập nhật nhà hàng đều phải duy trì cùng tên, thành phố, giá và danh mục của các trường đã có sẵn trong cơ sở dữ liệu.

Để tìm hiểu thêm về những việc bạn có thể làm với quy tắc bảo mật, hãy xem tài liệu này.

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 vào các tài nguyên sau: