Thông tin về lớp học lập trình này
1. Tổng quan
Bàn thắng
Trong lớp học lập trình này, bạn sẽ tạo một ứng dụng đề xuất nhà hàng dựa trên 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ừ mộ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 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ải dự án mẫu xuống
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 sẽ biên dịch chính xác và gặp sự cố ngay khi khởi chạy, vì ứng dụng thiếu tệp GoogleService-Info.plist
. Chúng ta sẽ sửa đổi thông tin đó trong bước tiếp theo.
3. Thiết lập Firebase
Tạo một dự án Firebase
- Đăng nhập vào bảng điều khiển của Firebase bằng Tài khoản Google của bạn.
- Nhấp vào nút này để tạo một dự án mới, rồi nhập tên dự án (ví dụ:
FriendlyEats
).
- Nhấp vào Tiếp tục.
- Nếu được nhắc, hãy xem xét và chấp nhận các điều khoản của Firebase, rồi nhấp vào Tiếp tục.
- (Không bắt buộc) Bật tính năng hỗ trợ của AI trong bảng điều khiển của Firebase (còn gọi là "Gemini trong Firebase").
- Đối với lớp học lập trình này, bạn không cần Google Analytics, vì vậy hãy tắt lựa chọn Google Analytics.
- Nhấp vào Tạo dự án, đợi dự án được cấp phép rồi nhấp vào Tiếp tục.
Kết nối ứng dụng của bạn với Firebase
Tạo một ứng dụng iOS trong dự án Firebase mới.
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 đị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 đăng nhập được, hãy đảm bảo rằng 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 cho 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ạ một thao tác ghi cơ bản vào Firestore.
Đố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 Firestore được chia thành các tài liệu, tập hợp và tập hợp con. Chúng ta sẽ lưu trữ mỗi 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 lấy một 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ó thể viết một số dữ liệu vì đã có một tham chiếu đến bộ sưu tập. 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 được lấy từ một từ điển mà chúng ta nhận được từ một cấu trúc Nhà hàng.
Chúng ta sắp 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ả những phần nào trong cơ sở dữ liệu mà người dùng nào có thể ghi. Hiện tại, chúng ta 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 có phần quá dễ dãi đối với một ứng dụng phát hành công khai, nhưng trong quá trình tạo ứng dụng, chúng ta muốn một thứ gì đó đủ 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. Khi kết thúc lớp học lập trình này, chúng ta sẽ nói 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 rồi 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. Nút này sẽ tạo một nhóm tài liệu nhà hàng, mặc dù bạn chưa thấy nhóm 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. Lúc này, 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 nội dung 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 thuộc bộ sưu tập cấp cao nhất có tên là "restaurants". Giờ đây, khi đã có một truy vấn, chúng ta cần đính kèm một trình nghe dữ liệu tức thời để tải dữ liệu từ Firestore vào ứng dụng. Hãy 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()
}
Đoạn mã trên 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 một trình nghe ảnh chụp nhanh vào truy vấn. Trình nghe này sẽ cập nhật bộ điều khiển khung hiển thị mỗi khi dữ liệu thay đổi trên máy chủ. Chúng tôi 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 dữ liệu tức thời 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, điều quan trọng là ứng dụng của chúng ta có thể xử lý các thay đổi.
Sau khi ánh xạ các từ điển của chúng ta sang cấu trúc (xem Restaurant.swift
), việc hiển thị dữ liệu chỉ là vấn đề gán một số thuộc tính của khung 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 dữ liệu 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 chịu trách nhiệm ánh xạ tập hợp các kiểu giá trị từ trước đó đến từng ô riêng lẻ 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 đã 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, tức là ứ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 tôi hiển thị danh sách các nhà hàng, nhưng người dùng không thể lọc theo 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.
Sau đâ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 của nó, phương thức whereField(_:isEqualTo:)
sẽ giúp truy vấn của chúng ta chỉ tải xuống những thành phần trong bộ sưu tập có các trường đáp ứng những 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"
.
Trong ứng dụng này, người dùng có thể kết hợp nhiều bộ lọc để tạo các cụm từ tìm kiếm cụ thể, chẳng hạn như "Pizza ở San Francisco" hoặc "Hải sả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 kết hợp duy nhất dựa trên dữ liệu đầu vào của người dùng. Giờ đây, truy vấn của chúng ta sẽ chỉ trả về những nhà hàng đáp ứng 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 quá trình 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 kết hợp. Việc yêu cầu chỉ mục trên các truy vấn giúp Firestore duy trì tốc độ nhanh ở quy mô lớn. Khi bạn mở đường liên kết trong thông báo lỗi, giao diện người dùng tạo chỉ mục sẽ tự động mở trong bảng điều khiển của Firebase với các thông 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 này.
7. Ghi dữ liệu trong một giao dịch
Trong phần này, chúng ta sẽ thêm chức 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ả các thao tác ghi của chúng ta đều là nguyên tử và tương đối đơn giản. Nếu có bất kỳ yêu cầu nào trong số đó gặp lỗi, có thể chúng tôi sẽ 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 lượt đọc và ghi. Trước tiên, bạn phải gửi bài đánh giá, sau đó số lượng bài đánh giá và điểm xếp hạng trung bình của nhà hàng cần được cập nhật. Nếu một trong hai thao tác này không thành công nhưng thao tác còn lại thành công, chúng ta sẽ rơi vào 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 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 mà chúng ta thực hiện bằng đối tượng giao dịch sẽ được Firestore coi là một thao tác cập nhật nguyên tử duy nhất. Nếu bản cập nhật không thành công trên máy chủ, Firestore sẽ tự động thử lại vài lần. Điều này có nghĩa là điều kiện lỗi của chúng ta có thể là một lỗi duy nhất xảy ra nhiều lần, 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.
8. Quy tắc bảo mật
Người dùng ứng dụng của chúng ta không được phép đọ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 xem xét kỹ hơn các quy tắc bảo mật mà chúng ta đã viết khi bắt đầu lớp học lập trình. 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 /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 chung có trong tất cả các quy tắc và điều kiện mà chúng ta đã 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 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 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 được nhiều việc hiệu quả hơn.
Chúng tôi muốn 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 đã 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 bài đánh giá gian lận.
Câu lệnh so khớp đầu tiên sẽ so khớp bộ sưu tập con có tên ratings
của mọi tài liệu thuộc bộ sưu tập restaurants
. Sau đó, điều kiện allow write
sẽ ngăn chặn việc gửi mọ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â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 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õ cam kết ngầm định 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á 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 của Firestore một cách chi tiết hơn để giới hạn các thao tác ghi trên từng trường trong tài liệu thay vì toàn bộ tài liệu. Chúng tôi có thể sử dụng thông tin 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 của một nhà hàng, loại bỏ khả năng người dùng có ý đồ xấu sửa đổ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 những thao tác được phép. Mọi người dùng đều 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 sẵn mà chúng ta đã tạo khi bắt đầu lớp học lập trình. Tuy nhiên, sau khi ghi một nhà hàng, tên, vị trí, giá và danh mục của nhà hàng đó sẽ không thể thay đổi. 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 giữ nguyên 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 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, đồng thời tìm hiểu 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 hoàn chỉnh 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: