1. Tổng quan
Bàn thắng
Trong lớp học lập trình này, bạn sẽ xây dựng một ứng dụng đề xuất nhà hàng được hỗ trợ bởi Firestore trên iOS bằng Swift. Bạn sẽ tìm hiểu cách:
- Đọc và ghi dữ liệu vào Firestore từ ứng dụng iOS
- Theo dõi các thay đổi trong dữ liệu Firestore theo thời gian thực
- Sử dụng tính năng Xác thực Firebase và các quy tắc bảo mật để bảo mật dữ liệu Firestore
- Viết 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 (trở lên)
- CocoaPods 1.12.0 (trở lên)
2. Tạo dự án trên bảng điều khiển Firebase
Thêm Firebase vào dự án
- Truy cập vào bảng điều khiển của Firebase.
- Chọn Create New Project (Tạo dự án mới) rồi đặt tên dự án là "Firestore iOS Codelab" (Lớp học lập trình iOS về Firestore).
3. Tải dự án mẫu
Tải mã xuống
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 (Cmd+R). Ứng dụng sẽ biên dịch chính xác và gặp sự cố ngay khi khởi chạy vì thiếu tệp GoogleService-Info.plist
. Chúng ta 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 để tạo dự án Firestore mới. Sau khi 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 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ụ 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 bằng Email/Mật khẩu trong phần Xác thực của bảng điều khiển Firebase.
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 dữ liệu 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 việc này trong chính ứng dụng để minh hoạ một hoạt động ghi cơ bản trên Firestore.
Đối tượng mô hình chính trong ứng dụng của chúng ta là nhà hàng. Dữ liệu Firestore được chia thành tài liệu, tập hợp và tập hợp con. Chúng ta sẽ lưu trữ từng nhà hàng dưới dạng một tài liệu trong một 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 về tài liệu và bộ sưu tậ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 bộ sưu tập nhà hàng. Thêm nội dung 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 tập hợp, chúng ta có thể ghi một số dữ liệu. Thêm nội dung 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)
Đoạn mã ở trên sẽ 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 ta lấy từ cấu trúc Nhà hàng.
Chúng ta đã gần đến đích – 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 của cơ sở dữ liệu mà người dùng nào 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á dễ dãi đối với một ứng dụng phát hành chính thức, nhưng trong quá trình xây dựng ứng dụng, chúng ta muốn có một cách thức đủ thoải mái để không liên tục gặp phải các 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ẽ thảo luận về cách tăng cường 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 Firebase, hãy thêm các quy tắc sau, sau đó nhấp vào Xuất bản.
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; } } }
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 đó, hãy nhấn vào nút "Populate" (Điền sẵn) ở trên cùng bên trái để tạo một loạt tài liệu về nhà hàng, mặc dù bạn chưa thấy tài liệu này trong ứng dụng.
Tiếp theo, hãy chuyển đến thẻ dữ liệu Firestore trong bảng điều khiển của Firebase. Bây giờ, bạn sẽ thấy các mục mới trong bộ sưu tập nhà hàng:
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 ảnh chụp 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 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ờ, chúng ta đã có một truy vấn, chúng ta cần đính kèm 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 sẽ tải bộ sưu tập xuống từ Firestore và lưu trữ bộ sưu tập đó trong một mảng cục bộ. Lệnh gọi addSnapshotListener(_:)
sẽ thêm trình nghe ảnh chụp nhanh vào truy vấn. Truy vấn này sẽ cập nhật trình điều khiển thành phần hiển thị mỗi khi dữ liệu thay đổi trên máy chủ. Chúng tôi sẽ nhận được thông tin 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 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 ta có thể xử lý các thay đổi.
Sau khi liên kết từ điển với cấu trúc (xem Restaurant.swift
), việc hiển thị dữ liệu chỉ là việc chỉ định một vài thuộc tính 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 này được gọi từ phương thức tableView(_:cellForRowAtIndexPath:)
của nguồn dữ liệu chế độ xem theo bảng. Phương thức này sẽ xử lý việc liên kết tập hợp các loại giá trị trước đó với các ô riêng lẻ trong chế độ xem theo 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 đã xuất hiện trên trình mô phỏng hoặc thiết bị. Nếu bạn đã hoàn tất phần này, ứng dụng của bạn hiện đang đọc và ghi dữ liệu bằng Cloud Firestore!
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 nhà hàng, nhưng người dùng không có cách nào để lọc dựa trên nhu cầu của họ. 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ư tên gọi, phương thức whereField(_:isEqualTo:)
sẽ khiến truy vấn của chúng ta chỉ tải các thành viên của bộ sưu tậ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, ứng dụng sẽ chỉ tải những nhà hàng có category
là "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 cụm từ tìm kiếm cụ thể, chẳng hạn như "Pizza ở San Francisco" hoặc "Đồ biển ở Los Angeles được sắp xếp theo Mức độ phổ biến".
Mở RestaurantsTableViewController.swift
rồi 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 đề whereField
và order
để tạo một truy vấn phức hợp dựa trên dữ liệu đầ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 và xác minh rằng bạn có thể lọc theo giá, thành phố và danh mục (nhớ nhập chính xác tên danh mục và thành phố). Trong khi kiểm thử, bạn có thể thấy các lỗi trong nhật ký có dạ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 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 trên truy vấn giúp Firestore hoạt động nhanh trên quy mô lớn. Thao tá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 Firebase với các tham số chính xác được điền sẵn. Để tìm hiểu thêm về chỉ mục trong Firestore, hãy truy cập vào tài liệu.
7. Ghi dữ liệu trong một giao dịch
Trong phần này, chúng ta sẽ thêm tính năng cho phép người dùng gửi bài đánh giá về nhà hàng. Cho đến nay, tất cả các hoạt động ghi của chúng ta đều nguyên tử và tương đối đơn giản. Nếu có bất kỳ lỗi nào, chúng ta có thể chỉ 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 điều phối nhiều hoạt động đọc và ghi. Trước tiên, bạn phải gửi bài đánh giá, sau đó cập nhật số điểm xếp hạng và điểm xếp hạng trung bình của nhà hàng. Nếu một trong những thao tác này không thành công nhưng thao tác còn lại thành công, thì chúng ta sẽ ở trạng thái không nhất quán, trong đó dữ liệu ở một phần của cơ sở dữ liệu không khớp với dữ liệu ở phần khác.
May mắn thay, Firestore cung cấp chức năng giao dịch cho phép chúng ta thực hiện nhiều thao tác đọc và ghi trong một thao tác nguyên tử duy nhất, đảm bảo dữ liệu của chúng ta luôn nhất quán.
Thêm mã sau vào bên dưới tất cả các phần 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 thao tác chúng ta thực hiện bằ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 không cập nhật được 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à điều kiện lỗi của chúng ta rất có thể là một lỗi duy nhất xảy ra liên tục, 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 ta không được đọc và ghi mọi phần dữ liệu trong cơ sở dữ liệu. Ví dụ: mọi người đều có thể xem điểm xếp hạng của một 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ệc 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ợ để đảm bảo an toàn tuyệt đối. 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 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. Mở bảng điều khiển Firebase rồi chuyển đến Database (Cơ sở dữ liệu) > Rules (Quy tắc) trong thẻ Firestore.
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;
}
}
}
Biến request
trong các quy tắc là một biến toàn cục có trong tất cả các quy tắc và điều kiện mà chúng ta thêm vào đả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 giúp ngăn người dùng chưa 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 được nhiều việc mạnh mẽ hơn.
Chúng tôi muốn hạn chế việc ghi 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 đã 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 bài đánh giá gian lận.
Câu lệnh so khớp đầu tiên so khớp 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 bài đánh giá được gửi 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 rất hiệu quả đối với 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õ cam kết ngầm ẩn mà chúng tôi đã 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 tôi thêm chức năng chỉnh sửa hoặc xoá cho bài đánh giá, thì chính bộ quy tắc này cũng sẽ ngăn người dùng sửa đổi hoặc xoá bài đánh giá của người dùng khác. Tuy nhiên, bạn cũng có thể sử dụng các quy tắc Firestore theo cách chi tiết hơn để giới hạn hoạt động ghi trên từng trường trong tài liệu thay vì toàn bộ tài liệu. Chúng ta có thể sử dụng tính năng 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ố điểm 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í 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 quyền tạo và quyền cập nhật để có thể chỉ định 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, giữ nguyên 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, nhưng sau khi ghi một nhà hàng, bạn không thể thay đổi tên, vị trí, giá và danh mục của nhà hàng đó. 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 phải duy trì cùng một tên, thành phố, giá và danh mục của các trường hiện có 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 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 cũng như 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: