Cloud Firestore iOS Codelab

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 8.3 (atau lebih tinggi)
  • CocoaPods 1.2.1 (atau lebih tinggi)

2. Buat proyek konsol Firebase

Tambahkan Firebase ke proyek

  1. Pergi ke konsol Firebase .
  2. Pilih Buat Proyek baru dan beri nama proyek Anda "Firestore iOS Codelab".

3. Dapatkan Proyek Sampel

Unduh Kode

Mulailah dengan kloning proyek sampel dan berjalan pod update dalam direktori proyek:

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

Terbuka FriendlyEats.xcworkspace di Xcode dan menjalankannya (Cmd + R). Aplikasi ini harus mengkompilasi dengan benar dan segera crash pada peluncuran, karena itu hilang GoogleService-Info.plist berkas. Kami akan memperbaikinya di langkah berikutnya.

Siapkan Firebase

Ikuti dokumentasi untuk membuat proyek Firestore baru. Setelah Anda punya proyek Anda, men-download proyek Anda GoogleService-Info.plist file dari Firebase konsol dan tarik ke akar proyek Xcode. Jalankan proyek lagi untuk memastikan aplikasi dikonfigurasi dengan benar dan tidak lagi mogok saat diluncurkan. Setelah masuk, Anda akan melihat layar kosong seperti contoh di bawah ini. Jika Anda tidak dapat masuk, pastikan Anda telah mengaktifkan metode masuk dengan Email/Sandi di Firebase console di bawah Otentikasi.

10a0671ce8f99704.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 konsol , tapi kami akan melakukannya dalam aplikasi itu sendiri untuk menunjukkan Firestore tulis dasar.

Objek model utama di aplikasi kami adalah restoran. Data Firestore dibagi menjadi dokumen, koleksi, dan subkoleksi. Kami akan menyimpan masing-masing restoran sebagai dokumen koleksi tingkat atas disebut restaurants . Jika Anda ingin mempelajari lebih lanjut tentang model data Firestore, membaca tentang dokumen dan koleksi di dokumentasi .

Sebelum kita dapat menambahkan data ke Firestore, kita perlu mendapatkan referensi ke koleksi restoran. Menambahkan berikut ke dalam untuk loop di RestaurantsTableViewController.didTapPopulateButton(_:) metode.

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

Sekarang kita memiliki referensi koleksi, kita dapat menulis beberapa data. Tambahkan 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 kami dapatkan dari struct Restoran.

Kita hampir sampai–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 yang diautentikasi untuk membaca dan menulis ke seluruh database. Ini agak terlalu permisif untuk aplikasi produksi, tetapi 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 berbicara tentang cara memperkuat aturan keamanan Anda dan membatasi kemungkinan membaca dan menulis yang tidak diinginkan.

Dalam tab Aturan dari konsol Firebase menambahkan aturan berikut dan kemudian klik Publikasikan.

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 rinci nanti, tetapi jika Anda sedang terburu-buru, kita lihat di dokumentasi aturan keamanan .

Menjalankan aplikasi dan masuk. Kemudian tekan tombol "mengisi" di kiri atas, yang akan membuat batch dokumen restoran, meskipun Anda tidak akan melihat ini di app belum.

Berikutnya, menavigasi ke tab Data Firestore di konsol Firebase. Anda sekarang akan melihat entri baru di koleksi restoran:

Tangkapan Layar 07-07-06 pukul 12.45.38.png

Selamat, Anda baru saja menulis data ke Firestore dari aplikasi iOS! Di bagian berikutnya, 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 utama adalah membuat kueri dan menambahkan pendengar snapshot. Listener ini akan diberi tahu 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 pelaksanaan RestaurantsTableViewController.baseQuery() :

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

Kueri ini mengambil hingga 50 restoran dari koleksi tingkat atas bernama "restoran". Sekarang setelah kita memiliki kueri, kita perlu melampirkan pendengar snapshot untuk memuat data dari Firestore ke dalam aplikasi kita. Tambahkan kode berikut ke RestaurantsTableViewController.observeQuery() metode hanya setelah panggilan untuk 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. The addSnapshotListener(_:) panggilan menambahkan pendengar snapshot untuk query yang akan memperbarui tampilan kontroler setiap kali perubahan data di server. Kami mendapatkan pembaruan secara otomatis dan tidak perlu mendorong perubahan secara manual. Ingat, pendengar snapshot ini dapat dipanggil kapan saja sebagai akibat dari perubahan sisi server sehingga penting agar aplikasi kita dapat menangani perubahan.

Setelah pemetaan kamus kami untuk struct (lihat Restaurant.swift ), menampilkan data yang hanya masalah menempatkan pandangan beberapa properti. 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 populate ini disebut dari tampilan tabel sumber data tableView(_:cellForRowAtIndexPath:) metode, yang mengurus pemetaan koleksi jenis nilai dari sebelum ke sel tampilan tabel individu.

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

2ca7f8c6052f7f79.png

6. Menyortir dan Memfilter Data

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

Berikut adalah contoh kueri sederhana untuk mengambil semua restoran Dim Sum:

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

Seperti namanya, yang whereField(_:isEqualTo:) metode akan membuat kita Download permintaan hanya anggota dari koleksi yang bidang memenuhi pembatasan kita set. Dalam hal ini, hanya akan men-download restoran di mana category adalah "Dim Sum" .

Dalam aplikasi ini pengguna dapat menghubungkan beberapa filter untuk membuat kueri tertentu, seperti "Pizza di San Francisco" atau "Makanan Laut di Los Angeles dipesan berdasarkan Popularitas".

Terbuka RestaurantsTableViewController.swift dan tambahkan blok kode berikut ke 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 whereField dan order klausa untuk membangun sebuah query senyawa tunggal berdasarkan input 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 menguji 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/testapp-5d356/database/firestore/indexes?create_index=..." 
UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_index=...}

Ini karena Firestore memerlukan indeks untuk sebagian besar kueri gabungan. Memerlukan indeks pada kueri membuat Firestore tetap dalam skala besar. Membuka link dari pesan kesalahan secara otomatis akan membuka UI penciptaan indeks di konsol Firebase dengan parameter yang benar diisi. 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 salah satu dari mereka salah, kami mungkin hanya akan meminta pengguna untuk mencoba lagi atau mencoba lagi secara otomatis.

Untuk menambahkan peringkat ke restoran, kita perlu mengoordinasikan banyak pembacaan dan penulisan. Pertama, ulasan itu sendiri harus diserahkan, dan kemudian jumlah peringkat restoran dan peringkat rata-rata perlu diperbarui. Jika salah satu dari ini gagal tetapi tidak yang lain, kita akan berada dalam keadaan tidak konsisten di mana data di satu bagian database kita tidak cocok dengan data di bagian lain.

Untungnya, Firestore menyediakan fungsionalitas transaksi yang memungkinkan kami melakukan banyak pembacaan dan penulisan dalam satu operasi atom, memastikan bahwa data kami tetap konsisten.

Tambahkan kode berikut di bawah ini semua deklarasi membiarkan 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 kami lakukan menggunakan objek transaksi akan diperlakukan sebagai pembaruan atom tunggal oleh Firestore. Jika pembaruan gagal di server, Firestore akan secara otomatis mencoba lagi beberapa kali. Ini berarti bahwa kondisi kesalahan kami kemungkinan besar adalah kesalahan tunggal yang terjadi berulang kali, misalnya jika perangkat benar-benar offline atau pengguna tidak diizinkan untuk menulis ke jalur yang mereka coba tulis.

8. Aturan keamanan

Pengguna aplikasi kami seharusnya tidak dapat membaca dan menulis setiap bagian data dalam database kami. Misalnya setiap orang harus dapat melihat peringkat restoran, tetapi hanya pengguna yang diautentikasi yang diizinkan untuk memposting peringkat. Menulis kode yang baik di klien tidak cukup, kita perlu menentukan model keamanan data kita 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 kami tulis di awal codelab. Buka konsol Firebase dan menavigasi ke database> Aturan dalam 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;
    }
  }
}

The request variabel dalam aturan di atas adalah variabel global tersedia di semua aturan, dan kami bersyarat menambahkan memastikan bahwa permintaan tersebut dikonfirmasi sebelum mengizinkan pengguna untuk melakukan apa pun. Ini mencegah pengguna yang tidak diautentikasi menggunakan Firestore API untuk membuat perubahan tidak sah pada data Anda. Ini adalah awal yang baik, tetapi kita dapat menggunakan aturan Firestore untuk melakukan hal-hal yang jauh lebih hebat.

Mari batasi penulisan ulasan sehingga ID pengguna ulasan harus cocok dengan ID pengguna yang diautentikasi. Ini memastikan bahwa pengguna tidak dapat meniru 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 pertandingan pertama sesuai dengan subcollection bernama ratings dari dokumen milik restaurants koleksi. The allow write kondisional kemudian mencegah setiap review dari yang disampaikan jika ID pengguna tinjauan ini tidak sesuai dengan yang pengguna. Pernyataan kecocokan kedua memungkinkan setiap pengguna yang diautentikasi untuk membaca dan menulis restoran ke database.

Ini bekerja sangat baik untuk ulasan kami, karena kami telah menggunakan aturan keamanan untuk secara eksplisit menyatakan jaminan implisit yang kami tulis ke dalam aplikasi kami sebelumnya–bahwa pengguna hanya dapat menulis ulasan mereka sendiri. Jika kami menambahkan fungsi edit atau hapus untuk ulasan, kumpulan aturan yang sama persis ini juga akan mencegah pengguna mengubah atau menghapus ulasan pengguna lain. Tetapi aturan Firestore juga dapat digunakan dengan cara yang lebih terperinci untuk membatasi penulisan pada masing-masing bidang dalam dokumen daripada seluruh dokumen itu sendiri. Kami dapat menggunakan ini untuk memungkinkan pengguna memperbarui hanya peringkat, peringkat rata-rata, dan jumlah peringkat untuk 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 telah membagi izin menulis kami menjadi membuat dan memperbarui sehingga kami dapat lebih spesifik tentang operasi mana yang harus diizinkan. Setiap pengguna dapat menulis restoran ke database, mempertahankan fungsi tombol Isi yang kami buat di awal codelab, tetapi 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 bidang yang sudah ada di database.

Untuk mempelajari lebih lanjut tentang apa yang dapat Anda lakukan dengan aturan keamanan, kita lihat di 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 penuh pada codelab-complete cabang .

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