Codelab iOS Cloud Firestore

1. Ringkasan

Sasaran

Dalam codelab ini, Anda akan mem-build aplikasi rekomendasi restoran yang didukung Firestore di iOS dalam Swift. Anda akan mempelajari cara:

  1. Membaca dan menulis data ke Firestore dari aplikasi iOS
  2. Memproses perubahan pada data Firestore secara real time
  3. Menggunakan aturan keamanan dan Firebase Authentication untuk mengamankan data Firestore
  4. Menulis kueri Firestore yang kompleks

Prasyarat

Sebelum memulai codelab ini, pastikan Anda telah menginstal:

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

2. Membuat project Firebase console

Menambahkan Firebase ke project

  1. Buka Firebase console.
  2. Pilih Create New Project dan beri nama project Anda "Firestore iOS Codelab".

3. Mendapatkan Project Contoh

Mendownload Kode

Mulailah dengan meng-clone project contoh dan menjalankan pod update di direktori project:

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 mengalami error saat diluncurkan, karena tidak ada file GoogleService-Info.plist. Kami akan memperbaikinya di langkah berikutnya.

Menyiapkan Firebase

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

d5225270159c040b.png

4. Menulis Data ke Firestore

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

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

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

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

Setelah memiliki referensi koleksi, kita dapat menulis beberapa data. Tambahkan kode berikut tepat setelah baris terakhir kode 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 dapatkan dari struct Restaurant.

Kita hampir selesai. Sebelum dapat menulis dokumen ke Firestore, kita perlu membuka aturan keamanan Firestore dan menjelaskan bagian database mana yang dapat ditulis oleh pengguna mana. Untuk saat ini, kita hanya akan mengizinkan pengguna yang terautentikasi untuk membaca dan menulis ke seluruh database. Hal ini sedikit terlalu permisif untuk aplikasi produksi, tetapi selama proses pembuatan aplikasi, kita menginginkan sesuatu yang cukup santai sehingga kita tidak akan terus-menerus mengalami masalah autentikasi saat bereksperimen. Di akhir codelab ini, kita akan membahas cara meningkatkan keamanan aturan keamanan Anda dan membatasi kemungkinan pembacaan dan penulisan yang tidak diinginkan.

Di tab Rules 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, tetapi jika Anda terburu-buru, lihat dokumentasi aturan keamanan.

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

Selanjutnya, buka tab data Firestore di Firebase console. Sekarang Anda akan melihat entri baru di koleksi restoran:

Screen Shot 2017-07-06 at 12.45.38 PM.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 pemroses snapshot. Pemroses ini akan mendapatkan notifikasi dari semua data yang ada yang cocok dengan kueri dan menerima pembaruan secara real time.

Pertama, mari kita buat kueri yang akan menayangkan daftar restoran yang default dan tidak difilter. Lihat implementasi RestaurantsTableViewController.baseQuery():

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

Kueri ini mengambil data hingga 50 restoran dari koleksi tingkat atas yang bernama "restaurants". Setelah memiliki kueri, kita perlu melampirkan pemroses snapshot untuk memuat data dari Firestore ke aplikasi. 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 mendownload koleksi dari Firestore dan menyimpannya dalam array secara lokal. Panggilan addSnapshotListener(_:) menambahkan pemroses snapshot ke kueri yang akan memperbarui pengontrol tampilan setiap kali data berubah di server. Kami mendapatkan pembaruan secara otomatis dan tidak perlu menerapkan perubahan secara manual. Ingat, pemroses snapshot ini bisa dipanggil kapan saja sebagai hasil dari perubahan sisi server sehingga penting agar aplikasi kita bisa menangani perubahan.

Setelah memetakan kamus kita ke struct (lihat Restaurant.swift), kita hanya perlu menampilkan data dengan 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 jenis nilai dari sebelumnya ke setiap sel tampilan tabel.

Jalankan kembali aplikasi dan pastikan 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. Mengurutkan dan Memfilter Data

Saat ini aplikasi kami menampilkan daftar restoran, tetapi pengguna tidak memiliki cara untuk memfilter restoran berdasarkan kebutuhan mereka. Di bagian ini, Anda akan menggunakan pembuatan kueri tingkat lanjut 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, metode whereField(_:isEqualTo:) akan membuat kueri kita hanya mendownload anggota koleksi yang kolomnya memenuhi batasan yang telah ditetapkan. Dalam hal ini, metode hanya akan mendownload restoran dengan category "Dim Sum".

Di aplikasi ini, pengguna dapat merangkai beberapa filter untuk membuat kueri tertentu, seperti "Pizza di San Francisco" atau "Makanan laut di Los Angeles yang dipesan berdasarkan Popularitas".

Buka RestaurantsTableViewController.swift dan tambahkan blok kode berikut di bagian 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 satu kueri gabungan berdasarkan input pengguna. Sekarang kueri kita hanya akan menampilkan restoran yang cocok dengan kebutuhan pengguna.

Jalankan project Anda dan pastikan Anda dapat memfilter berdasarkan harga, kota, dan kategori (pastikan untuk mengetik kategori dan nama kota dengan tepat). Saat menguji, Anda mungkin melihat error di log 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=...}

Ini karena Firestore memerlukan indeks untuk sebagian besar kueri gabungan. Mewajibkan indeks pada kueri membuat Firestore tetap cepat dalam skala besar. Membuka link dari pesan error akan otomatis membuka UI pembuatan indeks di Firebase console dengan parameter yang benar telah diisi. Untuk mempelajari indeks di Firestore lebih lanjut, buka dokumentasi.

7. Menulis data dalam transaksi

Di bagian ini, kita akan menambahkan kemampuan bagi pengguna untuk mengirim ulasan ke restoran. Sejauh ini, semua penulisan kita bersifat atomik dan relatif sederhana. Jika ada yang error, kita mungkin hanya meminta pengguna untuk mencobanya lagi atau mencobanya secara otomatis.

Untuk menambahkan rating ke restoran, kita perlu mengoordinasikan beberapa pembacaan dan penulisan. Pertama, ulasan itu sendiri harus dikirimkan, lalu jumlah rating dan rata-rata rating restoran perlu diperbarui. Jika salah satunya gagal, tetapi tidak, akan menghasilkan status yang tidak konsisten, yaitu data di satu bagian database tidak cocok dengan data di bagian yang lain.

Untungnya, Firestore menyediakan fungsi transaksi yang memungkinkan kita menjalankan beberapa pembacaan dan penulisan dalam satu operasi atomik, memastikan bahwa 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 buat menggunakan objek transaksi akan diperlakukan sebagai satu pembaruan atomik oleh Firestore. Jika update gagal di server, Firestore akan otomatis mencoba lagi beberapa kali. Artinya, kondisi error kita kemungkinan besar adalah satu error yang terjadi berulang kali, misalnya jika perangkat benar-benar offline atau pengguna tidak diberi otorisasi untuk menulis ke jalur yang mereka coba tulis.

8. Aturan keamanan

Pengguna aplikasi kita tidak boleh membaca dan menulis setiap bagian data dalam database kita. Misalnya, semua orang harus dapat melihat rating restoran, tetapi hanya pengguna terautentikasi yang diizinkan memposting rating. 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 pelajari lebih lanjut aturan keamanan yang kita tulis di awal codelab. Buka Firebase console dan buka 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 kondisional yang kita tambahkan memastikan bahwa permintaan telah diautentikasi sebelum mengizinkan pengguna melakukan apa pun. Tindakan ini mencegah pengguna yang tidak diautentikasi menggunakan Firestore API untuk melakukan perubahan yang tidak sah pada data Anda. Ini adalah awal yang baik, tetapi kita dapat menggunakan aturan Firestore untuk melakukan hal-hal yang jauh lebih canggih.

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

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 dari dokumen apa pun yang termasuk dalam koleksi restaurants. Kondisional allow write kemudian mencegah ulasan dikirim jika ID pengguna ulasan tidak cocok dengan ID pengguna. Pernyataan pencocokan kedua memungkinkan pengguna yang terautentikasi membaca dan menulis restoran ke database.

Ini sangat efektif untuk peninjauan kami, karena kami telah menggunakan aturan keamanan untuk secara eksplisit menyatakan jaminan implisit yang kami tulis ke dalam aplikasi 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. Namun, aturan Firestore juga dapat digunakan secara lebih terperinci untuk membatasi penulisan pada setiap kolom dalam dokumen, bukan seluruh dokumen itu sendiri. Kami dapat menggunakan ini untuk memungkinkan pengguna memperbarui hanya rating, rating rata-rata, dan jumlah rating untuk suatu restoran, sehingga menghapus kemungkinan pengguna berbahaya 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 kita membagi izin tulis menjadi {i>create<i} dan {i>update<i} sehingga kita dapat lebih spesifik tentang operasi mana yang harus diizinkan. Setiap pengguna dapat menulis restoran ke database, yang mempertahankan fungsi tombol Isi yang kita buat di awal codelab, tetapi setelah nama, lokasi, harga, dan kategori restoran ditulis, tidak dapat diubah. Lebih khusus lagi, aturan terakhir mengharuskan operasi pembaruan restoran untuk mempertahankan nama, kota, harga, dan kategori yang sama dari kolom yang sudah ada di database.

Untuk mempelajari lebih lanjut hal yang dapat Anda lakukan dengan aturan keamanan, lihat dokumentasinya.

9. Kesimpulan

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

Untuk mempelajari Firestore lebih lanjut, kunjungi referensi berikut: