Lab Kode iOS Cloud Firestore

1. Ikhtisar

Sasaran

Dalam codelab ini Anda akan membuat aplikasi rekomendasi restoran yang didukung Firestore di iOS di Swift. Anda akan belajar cara:

  1. Membaca dan menulis data ke Firestore dari aplikasi iOS
  2. Dengarkan perubahan data Firestore secara realtime
  3. Gunakan Firebase Authentication dan aturan keamanan untuk mengamankan data Firestore
  4. Tulis kueri Firestore yang kompleks

Prasyarat

Sebelum memulai codelab ini, pastikan Anda telah menginstal:

  • Xcode versi 14.0 (atau lebih tinggi)
  • CocoaPods 1.12.0 (atau lebih tinggi)

2. Buat proyek konsol Firebase

Tambahkan Firebase ke proyek

  1. Buka konsol Firebase .
  2. Pilih Buat Proyek Baru dan beri nama proyek Anda "Firestore iOS Codelab".

3. Dapatkan Contoh Proyek

Unduh Kode

Mulailah dengan mengkloning proyek sampel dan menjalankan pod update di direktori proyek:

git clone https://github.com/firebase/friendlyeats-ios
cd friendlyeats-ios
pod update

Buka FriendlyEats.xcworkspace di Xcode dan jalankan (Cmd+R). Aplikasi harus dikompilasi dengan benar dan langsung mogok saat diluncurkan, karena tidak ada file GoogleService-Info.plist . Kami akan memperbaikinya pada langkah berikutnya.

Siapkan Firebase

Ikuti dokumentasi untuk membuat proyek Firestore baru. Setelah Anda mendapatkan proyek, unduh file GoogleService-Info.plist proyek Anda dari Firebase console dan seret ke root proyek Xcode. Jalankan proyek lagi untuk memastikan aplikasi dikonfigurasi dengan benar dan tidak lagi error saat diluncurkan. Setelah login, Anda akan melihat layar kosong seperti contoh di bawah ini. Jika Anda tidak dapat masuk, pastikan Anda telah mengaktifkan metode masuk Email/Kata Sandi di Firebase console pada bagian Autentikasi.

d5225270159c040b.png

4. Tulis Data ke Firestore

Di bagian ini kita akan menulis beberapa data ke Firestore sehingga kita bisa mengisi UI aplikasi. Hal ini dapat dilakukan secara manual melalui Firebase console , namun kami akan melakukannya di aplikasi itu sendiri untuk mendemonstrasikan penulisan dasar Firestore.

Objek model utama di aplikasi kita adalah restoran. Data Firestore dibagi menjadi dokumen, koleksi, dan subkoleksi. Kami akan menyimpan setiap restoran sebagai dokumen dalam koleksi tingkat atas yang disebut restaurants . Jika Anda ingin mempelajari lebih lanjut model data Firestore, baca tentang dokumen dan koleksi di dokumentasi .

Sebelum kita dapat menambahkan data ke Firestore, kita perlu mendapatkan referensi koleksi restoran. Tambahkan baris berikut ke perulangan for bagian dalam dalam metode RestaurantsTableViewController.didTapPopulateButton(_:) .

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

Sekarang kita memiliki referensi koleksi, kita dapat menulis beberapa data. Tambahkan yang berikut ini tepat setelah baris kode terakhir yang kita tambahkan:

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)

Kode di atas menambahkan dokumen baru ke koleksi restoran. Data dokumen berasal dari kamus, yang kita peroleh dari struct Restoran.

Kita hampir mencapainya–sebelum kita dapat menulis dokumen ke Firestore, kita perlu membuka aturan keamanan Firestore dan menjelaskan bagian mana dari database kita yang dapat ditulisi oleh pengguna mana. Untuk saat ini, kami hanya mengizinkan pengguna terautentikasi untuk membaca dan menulis ke seluruh database. Ini agak terlalu permisif untuk aplikasi produksi, namun selama proses pembuatan aplikasi kami menginginkan sesuatu yang cukup santai sehingga kami tidak akan terus-menerus mengalami masalah autentikasi saat bereksperimen. Di akhir codelab ini, kita akan membahas cara memperketat aturan keamanan dan membatasi kemungkinan pembacaan dan penulisan yang tidak diinginkan.

Di tab Rules di Firebase console, tambahkan aturan berikut, lalu klik Publish .

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;
    }
  }
}

Kita akan membahas aturan keamanan secara mendetail nanti, namun jika Anda sedang terburu-buru, lihat dokumentasi aturan keamanan .

Jalankan aplikasi dan masuk. Lalu ketuk tombol " Isi " di kiri atas, yang akan membuat kumpulan dokumen restoran, meskipun Anda belum melihatnya di aplikasi.

Selanjutnya, navigasikan ke tab data Firestore di Firebase console. Anda sekarang akan melihat entri baru di koleksi restoran:

Tangkapan Layar 06-07-2017 pukul 12.45.38.png

Selamat, Anda baru saja menulis data ke Firestore dari aplikasi iOS! Di bagian selanjutnya Anda akan mempelajari cara mengambil data dari Firestore dan menampilkannya di aplikasi.

5. Menampilkan Data dari Firestore

Di bagian ini Anda akan mempelajari cara mengambil data dari Firestore dan menampilkannya di aplikasi. Dua langkah utamanya adalah membuat kueri dan menambahkan pemroses snapshot. Listener ini akan diberitahu tentang semua data yang ada yang cocok dengan kueri dan menerima pembaruan secara real time.

Pertama, mari buat kueri yang akan menyajikan daftar restoran default tanpa filter. Lihatlah implementasi RestaurantsTableViewController.baseQuery() :

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

Kueri ini mengambil hingga 50 restoran dari koleksi tingkat atas bernama "restoran". Sekarang kita memiliki pertanyaan, kita perlu melampirkan pemroses snapshot untuk memuat data dari Firestore ke dalam aplikasi kita. Tambahkan kode berikut ke metode RestaurantsTableViewController.observeQuery() tepat setelah panggilan ke 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()
}

Kode di atas mengunduh koleksi dari Firestore dan menyimpannya dalam array secara lokal. Panggilan addSnapshotListener(_:) menambahkan pendengar snapshot ke kueri yang akan memperbarui pengontrol tampilan setiap kali data berubah di server. Kami mendapatkan pembaruan secara otomatis dan tidak perlu melakukan perubahan secara manual. Ingat, pemroses snapshot ini dapat dipanggil kapan saja sebagai akibat dari perubahan di sisi server, jadi penting bagi aplikasi kita untuk dapat menangani perubahan.

Setelah memetakan kamus kita ke struct (lihat Restaurant.swift ), menampilkan data hanyalah masalah menetapkan beberapa properti tampilan. Tambahkan baris berikut ke RestaurantTableViewCell.populate(restaurant:) di 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)

Metode pengisian ini dipanggil dari metode tableView(_:cellForRowAtIndexPath:) sumber data tampilan tabel, yang menangani pemetaan kumpulan tipe nilai dari sebelumnya ke sel tampilan tabel individual.

Jalankan kembali aplikasi dan verifikasi bahwa restoran yang kita lihat sebelumnya di konsol kini terlihat di simulator atau perangkat. Jika Anda berhasil menyelesaikan bagian ini, aplikasi Anda sekarang membaca dan menulis data dengan Cloud Firestore!

391c0259bf05ac25.png

6. Menyortir dan Memfilter Data

Saat ini aplikasi kami menampilkan daftar restoran, namun tidak ada cara bagi pengguna untuk memfilter berdasarkan kebutuhan mereka. Di bagian ini Anda akan menggunakan kueri lanjutan Firestore untuk mengaktifkan pemfilteran.

Berikut ini contoh query sederhana untuk mengambil semua restoran Dim Sum:

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

Sesuai dengan namanya, metode whereField(_:isEqualTo:) akan membuat kueri kita hanya mengunduh anggota koleksi yang bidangnya memenuhi batasan yang kita tetapkan. Dalam hal ini, ia hanya akan mengunduh restoran dengan category "Dim Sum" .

Dalam aplikasi ini pengguna dapat merangkai beberapa filter untuk membuat kueri spesifik, seperti "Pizza di San Francisco" atau "Makanan Laut di Los Angeles yang diurutkan berdasarkan Popularitas".

Buka RestaurantsTableViewController.swift dan tambahkan blok kode berikut di tengah 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)
}

Cuplikan di atas menambahkan beberapa klausa whereField dan order untuk membuat kueri gabungan tunggal berdasarkan masukan pengguna. Sekarang kueri kami hanya akan mengembalikan restoran yang sesuai dengan kebutuhan pengguna.

Jalankan proyek Anda dan verifikasi bahwa Anda dapat memfilter berdasarkan harga, kota, dan kategori (pastikan untuk mengetikkan kategori dan nama kota dengan tepat). Saat pengujian, Anda mungkin melihat kesalahan di log Anda yang terlihat seperti ini:

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=...}

Hal ini karena Firestore memerlukan indeks untuk sebagian besar kueri gabungan. Mewajibkan indeks pada kueri membuat Firestore tetap cepat dalam skala besar. Membuka tautan dari pesan kesalahan akan secara otomatis membuka UI pembuatan indeks di Firebase console dengan parameter yang diisi dengan benar. Untuk mempelajari lebih lanjut tentang indeks di Firestore, kunjungi dokumentasi .

7. Menulis data dalam suatu transaksi

Di bagian ini, kami akan menambahkan kemampuan bagi pengguna untuk mengirimkan ulasan ke restoran. Sejauh ini, semua tulisan kami bersifat atomik dan relatif sederhana. Jika ada yang error, kemungkinan besar kami akan meminta pengguna untuk mencobanya lagi atau mencobanya lagi secara otomatis.

Untuk menambahkan peringkat ke sebuah restoran, kita perlu mengoordinasikan beberapa pembacaan dan penulisan. Pertama, ulasan itu sendiri harus dikirimkan, lalu jumlah rating dan rating rata-rata restoran perlu diperbarui. Jika salah satu gagal namun yang lain tidak, kita akan berada dalam keadaan tidak konsisten dimana data di satu bagian database kita tidak cocok dengan data di bagian lain.

Untungnya, Firestore menyediakan fungsionalitas transaksi yang memungkinkan kita melakukan beberapa pembacaan dan penulisan dalam satu operasi atom, sehingga memastikan data kita tetap konsisten.

Tambahkan kode berikut di bawah semua deklarasi let di 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)
    }
  }
}

Di dalam blok pembaruan, semua operasi yang kita lakukan menggunakan objek transaksi akan diperlakukan sebagai pembaruan atom tunggal oleh Firestore. Jika pembaruan gagal di server, Firestore akan otomatis mencobanya lagi beberapa kali. Ini berarti bahwa kondisi kesalahan kita kemungkinan besar merupakan kesalahan tunggal yang terjadi berulang kali, misalnya jika perangkat benar-benar offline atau pengguna tidak berwenang untuk menulis ke jalur yang mereka coba tulis.

8. Aturan keamanan

Pengguna aplikasi kami tidak boleh membaca dan menulis setiap bagian data di database kami. Misalnya setiap orang dapat melihat peringkat restoran, namun hanya pengguna terautentikasi yang boleh memposting peringkat. Menulis kode yang baik di klien saja tidak cukup, kita perlu menentukan model keamanan data di backend agar benar-benar aman. Di bagian ini kita akan mempelajari cara menggunakan aturan keamanan Firebase untuk melindungi data kita.

Pertama, mari kita lihat lebih dalam aturan keamanan yang kita tulis di awal codelab. Buka Firebase console dan navigasikan ke Database > Rules di 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;
    }
  }
}

Variabel request dalam aturan di atas adalah variabel global yang tersedia di semua aturan, dan kondisi yang kami tambahkan memastikan bahwa permintaan diautentikasi sebelum mengizinkan pengguna melakukan apa pun. Hal ini mencegah pengguna yang tidak diautentikasi menggunakan Firestore API untuk membuat perubahan tidak sah pada data Anda. Ini adalah awal yang baik, namun kita dapat menggunakan aturan Firestore untuk melakukan hal yang lebih efektif.

Mari batasi penulisan ulasan sehingga ID pengguna ulasan harus cocok dengan ID pengguna yang diautentikasi. Hal ini memastikan bahwa pengguna tidak dapat meniru identitas satu sama lain dan meninggalkan ulasan palsu. Ganti aturan keamanan Anda dengan yang berikut ini:

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;
    }
  }
}

Pernyataan kecocokan pertama cocok dengan subkoleksi bernama ratings dokumen apa pun yang termasuk dalam koleksi restaurants . Kondisi allow write kemudian mencegah ulasan apa pun dikirimkan jika ID pengguna ulasan tidak cocok dengan milik pengguna. Pernyataan kecocokan kedua memungkinkan pengguna yang diautentikasi untuk membaca dan menulis restoran ke database.

Ini bekerja dengan sangat baik untuk ulasan kami, karena kami telah menggunakan aturan keamanan untuk secara eksplisit menyatakan jaminan implisit yang kami tuliskan ke dalam aplikasi kami sebelumnya–bahwa pengguna hanya dapat menulis ulasan mereka sendiri. Jika kami menambahkan fungsi edit atau hapus untuk ulasan, rangkaian aturan yang sama juga akan mencegah pengguna mengubah atau menghapus ulasan pengguna lain. Namun aturan Firestore juga dapat digunakan secara lebih terperinci untuk membatasi penulisan pada masing-masing bidang dalam dokumen, bukan keseluruhan dokumen itu sendiri. Kami dapat menggunakan ini untuk memungkinkan pengguna memperbarui hanya peringkat, peringkat rata-rata, dan jumlah peringkat untuk sebuah restoran, menghilangkan kemungkinan pengguna jahat mengubah nama atau lokasi restoran.

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;
    }
  }
}

Di sini kami membagi izin menulis menjadi membuat dan memperbarui sehingga kami bisa lebih spesifik tentang operasi mana yang harus diizinkan. Setiap pengguna dapat menulis restoran ke database, dengan mempertahankan fungsi tombol Isi yang kita buat di awal codelab, namun setelah restoran ditulis, nama, lokasi, harga, dan kategorinya tidak dapat diubah. Lebih khusus lagi, aturan terakhir mengharuskan setiap operasi pembaruan restoran untuk mempertahankan nama, kota, harga, dan kategori yang sama dari kolom yang sudah ada di database.

Untuk mempelajari selengkapnya tentang apa yang dapat Anda lakukan dengan aturan keamanan, lihat dokumentasi .

9. Kesimpulan

Dalam codelab ini, Anda mempelajari cara membaca dan menulis dasar dan lanjutan dengan Firestore, serta cara mengamankan akses data dengan aturan keamanan. Anda dapat menemukan solusi lengkapnya di cabang codelab-complete .

Untuk mempelajari lebih lanjut tentang Firestore, kunjungi sumber daya berikut: