firebase firestore 총정리본

https://firebase.google.com/docs/firestore/quickstart

Get started with Cloud Firestore

This quickstart shows you how to set up Cloud Firestore, add data, then view the data you just added in the Firebase console.

Create a Cloud Firestore project

image

When you create a Cloud Firestore project, it also enables the API in the Cloud API Manager.

Set up your development environment

Add the required dependencies and client libraries to your app.

  1. Follow the instructions to add Firebase to your iOS app.
  2. Add the Cloud Firestore pod to your Podfile
  3. Save the file and run pod install.
pod 'Firebase/Core'
pod 'Firebase/Firestore'

Initialize Cloud Firestore

Initialize an instance of Cloud Firestore:

import Firebase

FirebaseApp.configure()

let db = Firestore.firestore()

Add data

Cloud Firestore stores data in Documents, which are stored in Collections. Cloud Firestore creates collections and documents implicitly the first time you add data to the document. You do not need to explicitly create collections or documents.

Create a new collection and a document using the following example code.

// Add a new document with a generated ID
var ref: DocumentReference? = nil
ref = db.collection("users").addDocument(data: [
   "first": "Ada",
   "last": "Lovelace",
   "born": 1815
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

Now add another document to the users collection. Notice that this document includes a key-value pair (middle name) that does not appear in the first document. Documents in a collection can contain different sets of information.

// Add a second document with a generated ID.
ref = db.collection("users").addDocument(data: [
   "first": "Alan",
   "middle": "Mathison",
   "last": "Turing",
   "born": 1912
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

Read data

To quickly verify that you’ve added data to Cloud Firestore, use the data viewer in the Firebase console.

You can also use the “get” method to retrieve the entire collection.

db.collection("users").getDocuments() { (querySnapshot, err) in
   if let err = err {
       print("Error getting documents: (err)")
   } else {
       for document in querySnapshot!.documents {
           print("(document.documentID) => (document.data())")
       }
   }
}


https://firebase.google.com/docs/firestore/data-model

References

Every document in Cloud Firestore is uniquely identified by its location within the database. The previous example showed a document alovelace within the collection users. To refer to this location in your code, you can create areference to it.

let alovelaceDocumentRef = db.collection("users").document("alovelace")

A reference is a lightweight object that just points to a location in your database. You can create a reference whether or not data exists there, and creating a reference does not perform any network operations.

You can also create references to collections:

let usersCollectionRef = db.collection("users")

For convenience, you can also create references by specifying the path to a document or collection as a string, with path components separated by a forward slash (/). For example, to create a reference to the alovelace document:

let aLovelaceDocumentReference = db.document("users/alovelace")

https://firebase.google.com/docs/firestore/manage-data/data-types

This page describes the data types that Cloud Firestore supports.

image

image

Value type ordering

When a query involves a field with values of mixed types, Cloud Firestore uses a deterministic ordering based on the internal representations. The following list shows the order:

  1. Null values
  2. Boolean values
  3. Integer and floating-point values, sorted in numerical order
  4. Date values
  5. Text string values
  6. Byte values
  7. Cloud Firestore references
  8. Geographical point values
  9. Array values
  10. Map values

https://firebase.google.com/docs/firestore/manage-data/add-data

Add data to Cloud Firestore

Set a document

To create or overwrite a single document, use the set() method:

// Add a new document in collection "cities"
db.collection("cities").document("LA").setData([
   "name": "Los Angeles",
   "state": "CA",
   "country": "USA"
]) { err in
   if let err = err {
       print("Error writing document: (err)")
   } else {
       print("Document successfully written!")
   }
}

If the document does not exist, it will be created. If the document does exist, its contents will be overwritten with the newly provided data, unless you specify that the data should be merged into the existing document, as follows:

// Update one field, creating the document if it does not exist.
db.collection("cities").document("BJ").setData([ "capital": true ], merge: true)

If you’re not sure whether the document exists, pass the option to merge the new data with any existing document to avoid overwriting entire documents.

Data types

Cloud Firestore lets you write a variety of data types inside a document, including strings, booleans, numbers, dates, null, and nested arrays and objects. Cloud Firestore always stores numbers as doubles, regardless of what type of number you use in your code.

Custom objects

java로 만 가능하다.즉 android에서만 적용 가능

Add a document

When you use set() to create a document, you must specify an ID for the document to create. For example:

db.collection("cities").document("new-city-id").setData(data)

But sometimes there isn’t a meaningful ID for the document, and it’s more convenient to let Cloud Firestore auto-generate an ID for you. You can do this by calling add():

// Add a new document with a generated id.
var ref: DocumentReference? = nil
ref = db.collection("cities").addDocument(data: [
   "name": "Tokyo",
   "country": "Japan"
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

In some cases, it can be useful to create a document reference with an auto-generated ID, then use the reference later. For this use case, you can call doc():

let newCityRef = db.collection("cities").document()

// later...
newCityRef.setData([
   // ...
])

Behind the scenes, .add(...) and .doc().set(...) are completely equivalent, so you can use whichever is more convenient.

Update a document

To update some fields of a document without overwriting the entire document, use the update() method:

let washingtonRef = db.collection("cities").document("DC")

// Set the "capital" field of the city 'DC'
washingtonRef.updateData([
   "capital": true
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Update fields in nested objects

If your document contains nested objects, you can use “dot notation” to reference nested fields within the document when you call update():

// Create an initial document to update.
let frankDocRef = db.collection("users").document("frank")
frankDocRef.setData([
   "name": "Frank",
   "favorites": [ "food": "Pizza", "color": "Blue", "subject": "recess" ],
   "age": 12
   ])

// To update age and favorite color:
db.collection("users").document("frank").updateData([
   "age": 13,
   "favorites.color": "Red"
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

You can also add server timestamps to specific fields in your documents, to track when an update was received by the server:

db.collection("objects").document("some-id").updateData([
   "lastUpdated": FieldValue.serverTimestamp(),
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Update elements in an array

If your document contains an array field, you can use arrayUnion() and arrayRemove() to add and remove elements. arrayUnion() adds elements to an array but only elements not already present. arrayRemove() removes all instances of each given element.

let washingtonRef = db.collection("cities").document("DC")

// Atomically add a new region to the "regions" array field.
washingtonRef.updateData([
   "regions": FieldValue.arrayUnion(["greater_virginia"])
])

// Atomically remove a region from the "regions" array field.
washingtonRef.updateData([
   "regions": FieldValue.arrayRemove(["east_coast"])
])

Transactions & Batched Writes

https://firebase.google.com/docs/firestore/manage-data/transactions

Transactions and batched writes

  • Transactions: a transaction is a set of read and write operations on one or more documents.
  • Batched Writes: a batched write is a set of write operations on one or more documents.

Updating data with transactions

Using the Cloud Firestore client libraries, you can group multiple operations into a single transaction. Transactions are useful when you want to update a field’s value based on its current value, or the value of some other field. You could increment a counter by creating a transaction that reads the current value of the counter, increments it, and writes the new value to Cloud Firestore.

A transaction consists of any number of get() operations followed by any number of write operations such as set(),update(), or delete(). In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. This feature ensures that the transaction runs on up-to-date and consistent data.

Transactions never partially apply writes. All writes execute at the end of a successful transaction.

When using transactions, note that:

  • Read operations must come before write operations.
  • A function calling a transaction (transaction function) might run more than once if a concurrent edit affects a document that the transaction reads.
  • Transaction functions should not directly modify application state.
  • Transactions will fail when the client is offline.

The following example shows how to create and run a transaction:

let sfReference = db.collection("cities").document("SF")

db.runTransaction({ (transaction, errorPointer) -> Any? in
   let sfDocument: DocumentSnapshot
   do {
       try sfDocument = transaction.getDocument(sfReference)
   } catch let fetchError as NSError {
       errorPointer?.pointee = fetchError
       return nil
   }

   guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -1,
           userInfo: [
               NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot (sfDocument)"
           ]
       )
       errorPointer?.pointee = error
       return nil
   }

   transaction.updateData(["population": oldPopulation + 1], forDocument: sfReference)
   return nil
}) { (object, error) in
   if let error = error {
       print("Transaction failed: (error)")
   } else {
       print("Transaction successfully committed!")
   }
}

Passing information out of transactions

Do not modify application state inside of your transaction functions. Doing so will introduce concurrency issues, because transaction functions can run multiple times and are not guaranteed to run on the UI thread. Instead, pass information you need out of your transaction functions. The following example builds on the previous example to show how to pass information out of a transaction:

let sfReference = db.collection("cities").document("SF")

db.runTransaction({ (transaction, errorPointer) -> Any? in
   let sfDocument: DocumentSnapshot
   do {
       try sfDocument = transaction.getDocument(sfReference)
   } catch let fetchError as NSError {
       errorPointer?.pointee = fetchError
       return nil
   }

   guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -1,
           userInfo: [
               NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot (sfDocument)"
           ]
       )
       errorPointer?.pointee = error
       return nil
   }

   let newPopulation = oldPopulation + 1
   guard newPopulation <= 1000000 else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -2,
           userInfo: [NSLocalizedDescriptionKey: "Population (newPopulation) too big"]
       )
       errorPointer?.pointee = error
       return nil
   }

   transaction.updateData(["population": newPopulation], forDocument: sfReference)
   return newPopulation
}) { (object, error) in
   if let error = error {
       print("Error updating population: (error)")
   } else {
       print("Population increased to (object!)")
   }
}

Transaction failure

A transaction can fail for the following reasons:

  • The transaction contains read operations after write operations. Read operations must always come before any write operations.
  • The transaction read a document that was modified outside of the transaction. In this case, the transaction automatically runs again. The transaction is retried a finite number of times.

A failed transaction returns an error and does not write anything to the database. You do not need to roll back the transaction; Cloud Firestore does this automatically.

Batched writes

If you do not need to read any documents in your operation set, you can execute multiple write operations as a single batch that contains any combination of set(), update(), or delete() operations. A batch of writes completes atomically and can write to multiple documents.

Batched writes are also useful for migrating large data sets to Cloud Firestore. A batched write can contain up to 500 operations and batching operations together reduces connection overhead resulting in faster data migration.

Batched writes have fewer failure cases than transactions and use simpler code. They are not affected by contention issues, because they don’t depend on consistently reading any documents. Batched writes execute even when the user’s device is offline. The following example shows how to build and commit a batch of writes:

// Get new write batch
let batch = db.batch()

// Set the value of 'NYC'
let nycRef = db.collection("cities").document("NYC")
batch.setData([:], forDocument: nycRef)

// Update the population of 'SF'
let sfRef = db.collection("cities").document("SF")
batch.updateData(["population": 1000000 ], forDocument: sfRef)

// Delete the city 'LA'
let laRef = db.collection("cities").document("LA")
batch.deleteDocument(laRef)

// Commit the batch
batch.commit() { err in
   if let err = err {
       print("Error writing batch (err)")
   } else {
       print("Batch write succeeded.")
   }
}

Data validation for atomic operations

For mobile/web client libraries, you can validate data using Cloud Firestore Security Rules. You can ensure that related documents are always updated atomically and always as part of a transaction or batched write. Use the getAfter()security rule function to access and validate the state of a document after a set of operations completes but beforeCloud Firestore commits the operations.

For example, imagine that the database for the cities example also contains a countries collection. Each countrydocument uses a last_updated field to keep track of the last time any city related to that country was updated. The following security rules require that an update to a city document must also atomically update the related country’s last_updated field:

service cloud.firestore {
 match /databases/{database}/documents {
   // If you update a city doc, you must also
   // update the related country's last_updated field.
   match /cities/{city} {
     allow write: if request.auth.uid != null &&
       getAfter(
         /databases/$(database)/documents/countries/$(request.resource.data.country)
       ).data.last_updated == request.time;
   }

   match /countries/{country} {
     allow write: if request.auth.uid != null;
   }
 }
}

Security rules limits

In security rules for transactions or batched writes, there is a limit of 20 document access calls for the entire atomic operation in addition to the normal 10 call limit for each single document operation in the batch.

For example, consider the following rules for a chat application:

service cloud.firestore {
 match /databases/{db}/documents {
   function prefix() {
     return /databases/{db}/documents;
   }
   match /chatroom/{roomId} {
     allow read, write: if roomId in get(/$(prefix())/users/$(request.auth.uid)).data.chats
                           || exists(/$(prefix())/admins/$(request.auth.uid));
   }
   match /users/{userId} {
     allow read, write: if userId == request.auth.uid
                           || exists(/$(prefix())/admins/$(request.auth.uid));
   }
   match /admins/{userId} {
     allow read, write: if exists(/$(prefix())/admins/$(request.auth.uid));
   }
 }
}

The snippets below illustrate the number of document access calls used for a few data access patterns:

// 0 document access calls used, because the rules evaluation short-circuits
// before the exists() call is invoked.
db.collection('user').doc('myuid').get(...);

// 1 document access call used. The maximum total allowed for this call
// is 10, because it is a single document request.
db.collection('chatroom').doc('mygroup').get(...);

// Initializing a write batch...
var batch = db.batch();

// 2 document access calls used, 10 allowed.
var group1Ref = db.collection("chatroom").doc("group1");
batch.set(group1Ref, {msg: "Hello, from Admin!"});

// 1 document access call used, 10 allowed.
var newUserRef = db.collection("users").doc("newuser");
batch.update(newUserRef, {"lastSignedIn": new Date()});

// 1 document access call used, 10 allowed.
var removedAdminRef = db.collection("admin").doc("otheruser");
batch.delete(removedAdminRef);

// The batch used a total of 2 + 1 + 1 = 4 document access calls, out of a total
// 20 allowed.
batch.commit();

Delete data from Cloud Firestore

https://firebase.google.com/docs/firestore/manage-data/delete-data

delete() 를 이용한다.

db.collection("cities").document("DC").delete() { err in
   if let err = err {
       print("Error removing document: (err)")
   } else {
       print("Document successfully removed!")
   }
}

Warning: Deleting a document does not delete its subcollections!

각각 하부의 subcollection은 손수 삭제해야 한다.


Delete fields

To delete specific fields from a document, use the FieldValue.delete() method when you update a document:

db.collection("cities").document("BJ").updateData([
   "capital": FieldValue.delete(),
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Delete collections

To delete an entire collection or subcollection in Cloud Firestore, retrieve all the documents within the collection or subcollection and delete them. If you have larger collections, you may want to delete the documents in smaller batches to avoid out-of-memory errors. Repeat the process until you’ve deleted the entire collection or subcollection.

Deleting a collection requires coordinating an unbounded number of individual delete requests. If you need to delete entire collections, do so only from a trusted server environment. While it is possible to delete a collection from a mobile/web client, doing so has negative security and performance implications.

The snippets below are somewhat simplified and do not deal with error handling, security, deleting subcollections, or maximizing performance. To learn more about one recommended approach to deleting collections in production, seeDeleting Collections and Subcollections.

Delete data with the Firebase CLI

You can also use the Firebase CLI to delete documents and collections. Use the following command to delete data:

firebase firestore:delete [options] <<path>>

Get data

https://firebase.google.com/docs/firestore/query-data/get-data

There are two ways to retrieve data stored in Cloud Firestore.

  • Call a method to get the data.
  • Set a listener to receive data-change events.

Get a document

The following example shows how to retrieve the contents of a single document using get():


let docRef = db.collection("cities").document("SF")

docRef.getDocument { (document, error) in
   if let document = document, document.exists {
       let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
       print("Document data: (dataDescription)")
   } else {
       print("Document does not exist")
   }
}

Note: If there is no document at the location referenced by docRef, the resulting document will be empty and calling existson it will return false.

Source Options

For platforms with offline support, you can set the source option to control how a get call uses the offline cache.

By default, a get call will attempt to fetch the latest document snapshot from your database. On platforms with offline support, the client library will use the offline cache if the network is unavailable or if the request times out.

You can specify the source option in a get() call to change the default behavior. You can fetch from only the database and ignore the offline cache, or you can fetch from only the offline cache. For example:

let docRef = db.collection("cities").document("SF")

// Force the SDK to fetch the document from the cache. Could also specify
// FirestoreSource.server or FirestoreSource.default.
docRef.getDocument(source: .cache) { (document, error) in
 if let document = document {
   let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
   print("Cached document data: (dataDescription)")
 } else {
   print("Document does not exist in cache")
 }
}

Custom objects

The previous example retrieved the contents of the document as a map, but in some languages it’s often more convenient to use a custom object type. In Add Data, you defined a City class that you used to define each city. You can turn your document back into a City object:

let docRef = db.collection("cities").document("BJ")

docRef.getDocument { (document, error) in
   if let city = document.flatMap({
     $0.data().flatMap({ (data) in
       return City(dictionary: data)
     })
   }) {
       print("City: (city)")
   } else {
       print("Document does not exist")
   }
}

Important: Each custom class must have a public constructor that takes no arguments. In addition, the class must include a public getter for each property.

Get multiple documents from a collection

You can also retrieve multiple documents with one request by querying documents in a collection. For example, you can use where() to query for all of the documents that meet a certain condition, then use get() to retrieve the results:

db.collection("cities").whereField("capital", isEqualTo: true)
   .getDocuments() { (querySnapshot, err) in
       if let err = err {
           print("Error getting documents: (err)")
       } else {
           for document in querySnapshot!.documents {
               print("(document.documentID) => (document.data())")
           }
       }
}

By default, Cloud Firestore retrieves all documents that satisfy the query in ascending order by document ID, but you can order and limit the data returned.

Get all documents in a collection

In addition, you can retrieve all documents in a collection by omitting the where() filter entirely:

db.collection("cities").getDocuments() { (querySnapshot, err) in
   if let err = err {
       print("Error getting documents: (err)")
   } else {
       for document in querySnapshot!.documents {
           print("(document.documentID) => (document.data())")
       }
   }
}

List subcollections of a document

The getCollections() method of the Cloud Firestore server client libraries lists all subcollections of a document reference.

Retrieving a list of collections is not possible with the mobile/web client libraries. You should only look up collection names as part of administrative tasks in trusted server environments. If you find that you need this capability in the mobile/web client libraries, consider restructuring your data so that subcollection names are predictable.

Get real time updates

https://firebase.google.com/docs/firestore/query-data/listen

db.collection("cities").document("SF")
   .addSnapshotListener { documentSnapshot, error in
     guard let document = documentSnapshot else {
       print("Error fetching document: (error!)")
       return
     }
     guard let data = document.data() else {
       print("Document data was empty.")
       return
     }
     print("Current data: (data)")
   }

Events for local changes

Local writes in your app will invoke snapshot listeners immediately. This is because of an important feature called “latency compensation.” When you perform a write, your listeners will be notified with the new data before the data is sent to the backend.

Retrieved documents have a metadata.hasPendingWrites property that indicates whether the document has local changes that haven’t been written to the backend yet. You can use this property to determine the source of events received by your snapshot listener:

db.collection("cities").document("SF")
   .addSnapshotListener { documentSnapshot, error in
       guard let document = documentSnapshot else {
           print("Error fetching document: (error!)")
           return
       }
       let source = document.metadata.hasPendingWrites ? "Local" : "Server"
       print("(source) data: (document.data() ?? [:])")
   }


Events for metadata changes

// Listen to document metadata.
db.collection("cities").document("SF")
   .addSnapshotListener(includeMetadataChanges: true) { documentSnapshot, error in
       // ...
   }

ref) https://stackoverflow.com/a/50186785

Listen to multiple documents in a collection

db.collection("cities").whereField("state", isEqualTo: "CA")
   .addSnapshotListener { querySnapshot, error in
       guard let documents = querySnapshot?.documents else {
           print("Error fetching documents: (error!)")
           return
       }
       let cities = documents.map { $0["name"]! }
       print("Current cities in CA: (cities)")
   }

The snapshot handler will receive a new query snapshot every time the query results change (that is, when a document is added, removed, or modified).

Important: As explained above under

Events for local changes

, you will receive events immediately for your local writes. Your listener can use the

metadata.hasPendingWrites

field on each document to determine whether the document has local changes that have not yet been written to the backend.

View changes between snapshots

document에 어떤 변화( add, modify,remove )가 있었는지를 파악하는 방법

db.collection("cities").whereField("state", isEqualTo: "CA")
    .addSnapshotListener { querySnapshot, error in
        guard let snapshot = querySnapshot else {
            print("Error fetching snapshots: (error!)")
            return
        }
        snapshot.documentChanges.forEach { diff in
            if (diff.type == .added) {
                print("New city: (diff.document.data())")
            }
            if (diff.type == .modified) {
                print("Modified city: (diff.document.data())")
            }
            if (diff.type == .removed) {
                print("Removed city: (diff.document.data())")
            }
        }
    }

Detach a listener

listener를 사용후 필요하지 않는 경우 제거해주어야 한다.

let listener = db.collection("cities").addSnapshotListener { querySnapshot, error in
   // ...
}

// ...

// Stop listening to changes
listener.remove()

Perform simple and compound queries

https://firebase.google.com/docs/firestore/query-data/queries

collection으로 부터 특정 조건이 맞는 document를 찾아내거나 추리거나 하는 작업을 query라고 한다. 

Simple queries

// Create a reference to the cities collection
let citiesRef = db.collection("cities")

// Create a query against the collection.
let query = citiesRef.whereField("state", isEqualTo: "CA")
let capitalCities = db.collection("cities").whereField("capital", isEqualTo: true)
citiesRef.whereField("state", isEqualTo: "CA")
citiesRef.whereField("population", isLessThan: 100000)
citiesRef.whereField("name", isGreaterThanOrEqualTo: "San Francisco")

Array membership

citiesRef
 .whereField("regions", arrayContains: "west_coast")

Compound queries

You can also chain multiple where() methods to create more specific queries (logical AND). However, to combine the equality operator (==) with a range or array-contains clause (<, <=, >, >=, or array_contains), make sure to create a composite index.

citiesRef
   .whereField("state", isEqualTo: "CO")
   .whereField("name", isEqualTo: "Denver")
citiesRef
   .whereField("state", isEqualTo: "CA")
   .whereField("population", isLessThan: 1000000)

You can only perform range comparisons (<, <=, >, >=) on a single field, and you can include at most one array_contains clause in a compound query:

citiesRef
   .whereField("state", isGreaterThanOrEqualTo: "CA")
   .whereField("state", isLessThanOrEqualTo: "IN")
citiesRef
   .whereField("state", isEqualTo: "CA")
   .whereField("population", isGreaterThan: 1000000)


틀린예)

citiesRef
   .whereField("state", isGreaterThanOrEqualTo: "CA")
   .whereField("population", isGreaterThan: 1000000)

Query limitations

Cloud Firestore does not support the following types of queries:

  • Queries with range filters on different fields, as described in the previous section.
  • Single queries across multiple collections or subcollections. Each query runs against a single collection of documents. For more information about how your data structure affects your queries, see Choose a Data Structure.
  • Logical OR queries. In this case, you should create a separate query for each OR condition and merge the query results in your app.
  • Queries with a != clause. In this case, you should split the query into a greater-than query and a less-than query. For example, although the query clause where("age", "!=", "30") is not supported, you can get the same result set by combining two queries, one with the clause where("age", "<", "30") and one with the clause where("age", ">", 30).

Order and limit data

https://firebase.google.com/docs/firestore/query-data/order-limit-data

Order and limit data

citiesRef.order(by: "name").limit(to: 3)
citiesRef.order(by: "name", descending: true).limit(to: 3)
citiesRef
   .order(by: "state")
   .order(by: "population", descending: true)

You can combine where() filters with orderBy() and limit(). In the following example, the queries define a population threshold, sort by population in ascending order, and return only the first few results that exceed the threshold:

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "population")
   .limit(to: 2)

However, if you have a filter with a range comparison (<, <=, >, >=), your first ordering must be on the same field:

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "population")

틀린예)

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "country")

Pagination

https://firebase.google.com/docs/firestore/query-data/query-cursors

Add a simple cursor to a query

// Get all cities with population over one million, ordered by population.
db.collection("cities")
   .order(by: "population")
   .start(at: [1000000])
// Get all cities with population less than one million, ordered by population.
db.collection("cities")
   .order(by: "population")
   .end(at: [1000000])

Use a document snapshot to define the query cursor

document를 cursor로 직접 이용하는 경우 ( start()에 직접 document obj를 pass 한다 )

db.collection("cities")
   .document("SF")
   .addSnapshotListener { (document, error) in
       guard let document = document else {
           print("Error retreving cities: (error.debugDescription)")
           return
       }

       // Get all cities with a population greater than or equal to San Francisco.
       let sfSizeOrBigger = db.collection("cities")
           .order(by: "population")
           .start(atDocument: document)
}

Paginate a query

// Construct query for first 25 cities, ordered by population
let first = db.collection("cities")
   .order(by: "population")
   .limit(to: 25)

first.addSnapshotListener { (snapshot, error) in
   guard let snapshot = snapshot else {
       print("Error retreving cities: (error.debugDescription)")
       return
   }

   guard let lastSnapshot = snapshot.documents.last else {
       // The collection is empty.
       return
   }

   // Construct a new query starting after this document,
   // retrieving the next 25 cities.
   let next = db.collection("cities")
       .order(by: "population")
       .start(afterDocument: lastSnapshot)

   // Use the query for pagination.
   // ...
}

Set multiple cursor conditions

조건에 해당하는 document가 여러개 인경우 조건을 좀더 명확하게 지정할수 있다.

// Will return all Springfields
db.collection("cities")
   .order(by: "name")
   .order(by: "state")
   .start(at: ["Springfield"])

// Will return "Springfield, Missouri" and "Springfield, Wisconsin"
db.collection("cities")
   .order(by: "name")
   .order(by: "state")
   .start(at: ["Springfield", "Missouri"])

index

https://firebase.google.com/docs/firestore/query-data/index-overview

Index types

Cloud Firestore uses two types of indexes, single-field and composite. Both types of indexes are uniquely defined by the fields they index and the index mode on each field.

Automatic indexing

By default, Cloud Firestore automatically maintains an index for each field in a document and each subfield in a map. Cloud Firestore uses the following settings for automatically created single-field indexes:

  • For each non-array and non-map field, Cloud Firestore defines two single-field indexes, one in ascending mode and one in descending mode.
  • For each map field, Cloud Firestore creates one ascending index and one descending index for each non-array and non-map subfield in the map.
  • For each array field in a document, Cloud Firestore creates and maintains an array-contains index.

If you need to run a compound query that uses a range comparison (<, <=, >, or >=) or if you need to sort by a different field, you must create a composite index for that query.


Composite indexes

A composite index stores a sorted mapping of all the documents in a collection that contain multiple specific fields instead of just one. A composite index also defines an index mode for each of its fields (ascending, descending, or array-contains), and the index is sorted based on the index modes.

Note: You can have at most one array field per composite index.



Security Rules

https://firebase.google.com/docs/firestore/security/get-started

Note: The server client libraries bypass all Cloud Firestore Security Rules and instead authenticate through Google Application Default Credentials. If you are using the server client libraries or the REST or RPC APIs, make sure to set up Cloud Identity and Access Management for Cloud Firestore.

server에서 접근하는 연결의 security rule은 IAM으로 한다.

예)

// Allow read/write access on all documents to any user signed in to the application
service cloud.firestore {
 match /databases/{database}/documents {
   match /{document=**} {
     allow read, write: if request.auth.uid != null;
   }
 }
}

CLI로 rule을 수정하는 경우 주의사항

Note: When you deploy security rules using the Firebase CLI, the rules defined in your project directory overwrite any existing rules in the Firebase console. So, if you choose to define or edit your security rules using the Firebase console, make sure that you also update the rules defined in your project directory.

Structuring Cloud Firestore Security Rules

https://firebase.google.com/docs/firestore/security/rules-structure

Service and database declaration

Cloud Firestore Security Rules always begin with the following declaration:

service cloud.firestore {
 match /databases/{database}/documents {
   // ...
 }
}

Basic read/write rules

service cloud.firestore {
 match /databases/{database}/documents {

   // Match any document in the 'cities' collection
   match /cities/{city} {
     allow read: if <condition>;
     allow write: if <condition>;
   }
 }
}

In the example above, the match statement uses the {city} wildcard syntax. This means the rule applies to any document in the cities collection, such as /cities/SF or /cities/NYC. When the allow expressions in the match statement are evaluated, the city variable will resolve to the city document name, such as SF or NYC.

Granular operations

service cloud.firestore {
 match /databases/{database}/documents {
   // A read rule can be divided into get and list rules
   match /cities/{city} {
     // Applies to single document read requests
     allow get: if <condition>;

     // Applies to queries and collection read requests
     allow list: if <condition>;
   }

   // A write rule can be divided into create, update, and delete rules
   match /cities/{city} {
     // Applies to writes to nonexistent documents
     allow create: if <condition>;

     // Applies to writes to existing documents
     allow update: if <condition>;

     // Applies to delete operations
     allow delete: if <condition>;
   }
 }
}

Hierarchical data

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     allow read, write: if <condition>;

       // Explicitly define rules for the 'landmarks' subcollection
       match /landmarks/{landmark} {
         allow read, write: if <condition>;
       }
   }
 }
}

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     match /landmarks/{landmark} {
       allow read, write: if <condition>;
     }
   }
 }
}

위와 아래는 같은 내용

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city}/landmarks/{landmark} {
     allow read, write: if <condition>;
   }
 }
}

If you want rules to apply to an arbitrarily deep hierarchy, use the recursive wildcard syntax, {name=**}:

service cloud.firestore {
 match /databases/{database}/documents {
   // Matches any document in the cities collection as well as any document
   // in a subcollection.
   match /cities/{document=**} {
     allow read, write: if <condition>;
   }
 }
}

When using the recursive wildcard syntax, the wildcard variable will contain the entire matching path segment, even if the document is located in a deeply nested subcollection. For example, the rules listed above would match a document located at /cities/SF/landmarks/coit_tower, and the value of the document variable would be SF/landmarks/coit_tower.

Recursive wildcards cannot match an empty path, so match /cities/{city}/{document=**} will match documents in subcollections but not in the cities collection, whereas match /cities/{document=**} will match both documents in the cities collection and subcollections.

It’s possible for a document to match more than one match statement. In the case where multiple allow expressions match a request, the access is allowed if any of the conditions is true:

service cloud.firestore {
 match /databases/{database}/documents {
   // Matches any document in the 'cities' collection.
   match /cities/{city} {
     allow read, write: if false;
   }

   // Matches any document in the 'cities' collection or subcollections.
   match /cities/{document=**} {
     allow read, write: if true;
   }
 }
}

In the example above, all reads and writes to the cities collection will be allowed because the second rule is always true, even though the first rule is always false.

firestore에서 사용되는 class reference doc

ref) https://firebase.google.com/docs/reference/security/database/

firestore security rules설정 예시

https://www.youtube.com/watch?v=b7PUm7LmAOw

Writing conditions for Cloud Firestore Security Rules

https://firebase.google.com/docs/firestore/security/rules-conditions

Authentication

One of the most common security rule patterns is controlling access based on the user’s authentication state. For example, your app may want to allow only signed-in users to write data:

service cloud.firestore {
 match /databases/{database}/documents {
   // Allow the user to access documents in the "cities" collection
   // only if they are authenticated.
   match /cities/{city} {
     allow read, write: if request.auth.uid != null;
   }
 }
}

자세한 Request class 내용

https://firebase.google.com/docs/reference/rules/rules.firestore.Request

If your app uses Firebase Authentication, the request.auth variable contains the authentication information for the client requesting data. For more information about request.auth, see the reference documentation.

service cloud.firestore {
 match /databases/{database}/documents {
   // Make sure the uid of the requesting user matches name of the user
   // document. The wildcard expression {userId} makes the userId variable
   // available in rules.
   match /users/{userId} {
     allow read, update, delete: if request.auth.uid == userId;
     allow create: if request.auth.uid != null;
   }
 }
}

Data validation

Many apps store access control information as fields on documents in the database. Cloud Firestore Security Rules can dynamically allow or deny access based on document data:

service cloud.firestore {
 match /databases/{database}/documents {
   // Allow the user to read data if the document has the 'visibility'
   // field set to 'public'
   match /cities/{city} {
     allow read: if resource.data.visibility == 'public';
   }
 }
}

The resource variable refers to the requested document, and resource.data is a map of all of the fields and values stored in the document. For more information on the resource variable, see the reference documentation.

When writing data, you may want to compare incoming data to existing data. In this case, if your ruleset allows the pending write, the request.resource variable contains the future state of the document. For update operations that only modify a subset of the document fields, the request.resource variable will contain the pending document state after the operation. You can check the field values in request.resource to prevent unwanted or inconsistent data updates:

service cloud.firestore {
 match /databases/{database}/documents {
   // Make sure all cities have a positive population and
   // the name is not changed
   match /cities/{city} {
     allow update: if request.resource.data.population > 0
                   && request.resource.data.name == resource.data.name;
   }
 }
}

Access other documents

Using the get() and exists() functions, your security rules can evaluate incoming requests against other documents in the database. The get() and exists() functions both expect fully specified document paths. When using variables to construct paths for get() and exists(), you need to explicitly escape variables using the $(variable) syntax.

In the example below, the database variable is captured by the match statement match /databases/{database}/documents and used to form the path:

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     // Make sure a 'users' document exists for the requesting user before
     // allowing any writes to the 'cities' collection
     allow create: if exists(/databases/$(database)/documents/users/$(request.auth.uid))

     // Allow the user to delete cities if their user document has the
     // 'admin' field set to 'true'
     allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
   }
 }
}

For writes, you can use the getAfter() function to access the state of a document after a transaction or batch of writes completes but before the transaction or batch commits. Like get(), the getAfter() function takes a fully specified document path. You can use getAfter() to define sets of writes that must take place together as a transaction or batch.

Custom functions

  • Functions can contain only a single return statement. They cannot contain any additional logic. For example, they cannot create intermediate variables, execute loops, or call external services.
  • Functions can automatically access functions and variables from the scope in which they are defined. For example, a function defined within the service cloud.firestore scope has access to the resource variable and built-in functions such as get() and exists().
  • Functions may call other functions but may not recurse. The total call stack depth is limited to 10.

A function is defined with the function keyword and takes zero or more arguments. For example, you may want to combine the two types of conditions used in the examples above into a single function:

service cloud.firestore {
 match /databases/{database}/documents {
   // True if the user is signed in or the requested data is 'public'
   function signedInOrPublic() {
     return request.auth.uid != null || resource.data.visibility == 'public';
   }

   match /cities/{city} {
     allow read, write: if signedInOrPublic();
   }

   match /users/{user} {
     allow read, write: if signedInOrPublic();
   }
 }
}

Securely query data

https://firebase.google.com/docs/firestore/security/rules-query

Queries and security rules

When writing queries to retrieve documents, keep in mind that security rules are not filters—queries are all or nothing. To save you time and resources, Cloud Firestore evaluates a query against its potential result set instead of the actual field values for all of your documents. If a query could potentially return documents that the client does not have permission to read, the entire request fails.

Note: This behavior applies to queries that retrieve one or more documents from a collection and not to individual document retrievals. When you use a document ID to retrieve a single document, Cloud Firestore reads the document and evaluates the request using your security rules and the actual document properties.

As the examples below demonstrate, you must write your queries to fit the constraints of your security rules.


Secure and query documents based on

auth.uid

The following example demonstrates how to write a query to retrieve documents protected by a security rule. Consider a database that contains a collection of story documents:

/stories/{storyid}

{
 title: "A Great Story",
 content: "Once upon a time...",
 author: "some_auth_id",
 published: false
}

In addition to the title and content fields, each document stores the author and published fields to use for access control. These examples assume the app uses Firebase Authentication to set the author field to the UID of the user who created the document. Firebase Authentication also populates the request.auth variable in the security rules.

The following security rule uses the request.auth and resource.data variables to restrict read and write access for each story to its author:

service cloud.firestore {
 match /databases/{database}/documents {
   match /stories/{storyid} {
     // Only the authenticated user who authored the document can read or write
     allow read, write: if request.auth.uid == resource.data.author;
   }
 }
}

Suppose that your app includes a page that shows the user a list of story documents that they authored. You might expect that you could use the following query to populate this page. However, this query will fail, because it does not include the same constraints as your security rules:

Invalid: Query constraints do not match security rules constraints

// This query will fail
db.collection("stories").get()

The query fails even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database. If a query could potentially include documents that violate your security rules, the query will fail.

In contrast, the following query succeeds, because it includes the same constraint on the author field as the security rules:

Valid: Query constraints match security rules constraints

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

Secure and query documents based on a field

To further demonstrate the interaction between queries and rules, the security rules below expand read access for the stories collection to allow any user to read story documents where the published field is set to true.

service cloud.firestore {
 match /databases/{database}/documents {
   match /stories/{storyid} {
     // Anyone can read a published story; only story authors can read unpublished stories
     allow read: if resource.data.published == true || request.auth.uid == resource.data.author;
     // Only story authors can write
     allow write: if request.auth.uid == resource.data.author;
   }
 }
}

The query for published pages must include the same constraints as the security rules:

db.collection("stories").where("published", "==", true).get()

The query constraint .where("published", "==", true) guarantees that resource.data.published is true for any result. Therefore, this query satisfies the security rules and is allowed to read data.

Evaluating constraints on queries

Your security rules can also accept or deny queries based on their constraints. The request.query variable contains the limit, offset, and orderBy properties of a query. For example, your security rules can deny any query that doesn’t limit the maximum number of documents retrieved to a certain range:

allow list: if request.query.limit <= 10;

Note:

You can break read rules into get and list rules

. Rules for

get

apply to requests for single documents, and rules for

list

apply to queries and requests for collections.

The following ruleset demonstrates how to write security rules that evaluate constraints placed on queries. This example expands the previous stories ruleset with the following changes:

  • The ruleset separates the read rule into rules for get and list.
  • The get rule restricts retrieval of single documents to public documents or documents the user authored.
  • The list rule applies the same restrictions as get but for queries. It also checks the query limit, then denies any query without a lmit or with a limit greater than 10.
  • The ruleset defines an authorOrPublished() function to avoid code duplication.
service cloud.firestore {

 match /databases/{database}/documents {

   match /stories/{storyid} {

     // Returns `true` if the requested story is 'published'
     // or the user authored the story
     function authorOrPublished() {
       return resource.data.published == true || request.auth.uid == resource.data.author;
     }

     // Deny any query not limited to 10 or fewer documents
     // Anyone can query published stories
     // Authors can query their unpublished stories
     allow list: if request.query.limit <= 10 &&
                    authorOrPublished();

     // Anyone can retrieve a published story
     // Only a story's author can retrieve an unpublished story
     allow get: if authorOrPublished();

     // Only a story's authors can write to a story
     allow write: if request.auth.uid == resource.data.author;
   }

 }
}

Enable offline data

https://firebase.google.com/docs/firestore/manage-data/enable-offline

Note: Offline persistence is supported only in Android, iOS, and web apps.

  • For Android and iOS, offline persistence is enabled by default. To disable persistence, set the PersistenceEnabled option to false.
  • For the web, offline persistence is disabled by default.
let settings = FirestoreSettings()
settings.isPersistenceEnabled = true

// Any additional options
// ...

// Enable offline data persistence
let db = Firestore.firestore()
db.settings = settings

Listen to offline data

While the device is offline, if you have enabled offline persistence, your listeners will receive listen events when the locally cached data changes. You can listen to documents, collections, and queries.

To check whether you’re receiving data from the server or the cache, use the fromCache property on the SnapshotMetadata in your snapshot event. If fromCache is true, the data came from the cache and might be stale or incomplete. If fromCache is false, the data is complete and current with the latest updates on the server.

By default, no event is raised if only the SnapshotMetadata changed. If you rely on the fromCache values, specify the includeMetadataChanges listen option when you attach your listen handler.

// Listen to metadata updates to receive a server snapshot even if
// the data is the same as the cached data.
db.collection("cities").whereField("state", isEqualTo: "CA")
   .addSnapshotListener(includeMetadataChanges: true) { querySnapshot, error in
       guard let snapshot = querySnapshot else {
           print("Error retreiving snapshot: (error!)")
           return
       }

       for diff in snapshot.documentChanges {
           if diff.type == .added {
               print("New city: (diff.document.data())")
           }
       }

       let source = snapshot.metadata.isFromCache ? "local cache" : "server"
       print("Metadata: Data fetched from (source)")
}

Get offline data

If you get a document while the device is offline, Cloud Firestore returns data from the cache. If the cache does not contain data for that document, or the document does not exist, the get call returns an error.

Query offline data

Querying works with offline persistence. You can retrieve the results of queries with either a direct get or by listening, as described in the preceding sections. You can also create new queries on locally persisted data while the device is offline, but the queries will initially run only against the cached documents.

Disable and enable network access

You can use method below to disable network access for your Cloud Firestore client. While network access is disabled, all snapshot listeners and document requests retrieve results from the cache. Write operations are queued until network access is re-enabled.

Firestore.firestore().disableNetwork { (error) in
   // Do offline things
   // ...
}

Use the following method to re-enable network access:

Firestore.firestore().enableNetwork { (error) in
   // Do online things
   // ...
}

스텝바이 스텝 노트

https://codelabs.developers.google.com/codelabs/firestore-ios/#0

실전 퀵 치트노트

https://firebaseopensource.com/projects/firebase/quickstart-ios/

.

firebase firestore 총정리본

https://firebase.google.com/docs/firestore/quickstart

Get started with Cloud Firestore

This quickstart shows you how to set up Cloud Firestore, add data, then view the data you just added in the Firebase console.

Create a Cloud Firestore project

image

When you create a Cloud Firestore project, it also enables the API in the Cloud API Manager.

Set up your development environment

Add the required dependencies and client libraries to your app.

  1. Follow the instructions to add Firebase to your iOS app.
  2. Add the Cloud Firestore pod to your Podfile
  3. Save the file and run pod install.
pod 'Firebase/Core'
pod 'Firebase/Firestore'

Initialize Cloud Firestore

Initialize an instance of Cloud Firestore:

import Firebase

FirebaseApp.configure()

let db = Firestore.firestore()

Add data

Cloud Firestore stores data in Documents, which are stored in Collections. Cloud Firestore creates collections and documents implicitly the first time you add data to the document. You do not need to explicitly create collections or documents.

Create a new collection and a document using the following example code.

// Add a new document with a generated ID
var ref: DocumentReference? = nil
ref = db.collection("users").addDocument(data: [
   "first": "Ada",
   "last": "Lovelace",
   "born": 1815
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

Now add another document to the users collection. Notice that this document includes a key-value pair (middle name) that does not appear in the first document. Documents in a collection can contain different sets of information.

// Add a second document with a generated ID.
ref = db.collection("users").addDocument(data: [
   "first": "Alan",
   "middle": "Mathison",
   "last": "Turing",
   "born": 1912
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

Read data

To quickly verify that you’ve added data to Cloud Firestore, use the data viewer in the Firebase console.

You can also use the “get” method to retrieve the entire collection.

db.collection("users").getDocuments() { (querySnapshot, err) in
   if let err = err {
       print("Error getting documents: (err)")
   } else {
       for document in querySnapshot!.documents {
           print("(document.documentID) => (document.data())")
       }
   }
}


https://firebase.google.com/docs/firestore/data-model

References

Every document in Cloud Firestore is uniquely identified by its location within the database. The previous example showed a document alovelace within the collection users. To refer to this location in your code, you can create areference to it.

let alovelaceDocumentRef = db.collection("users").document("alovelace")

A reference is a lightweight object that just points to a location in your database. You can create a reference whether or not data exists there, and creating a reference does not perform any network operations.

You can also create references to collections:

let usersCollectionRef = db.collection("users")

For convenience, you can also create references by specifying the path to a document or collection as a string, with path components separated by a forward slash (/). For example, to create a reference to the alovelace document:

let aLovelaceDocumentReference = db.document("users/alovelace")

https://firebase.google.com/docs/firestore/manage-data/data-types

This page describes the data types that Cloud Firestore supports.

image

image

Value type ordering

When a query involves a field with values of mixed types, Cloud Firestore uses a deterministic ordering based on the internal representations. The following list shows the order:

  1. Null values
  2. Boolean values
  3. Integer and floating-point values, sorted in numerical order
  4. Date values
  5. Text string values
  6. Byte values
  7. Cloud Firestore references
  8. Geographical point values
  9. Array values
  10. Map values

https://firebase.google.com/docs/firestore/manage-data/add-data

Add data to Cloud Firestore

Set a document

To create or overwrite a single document, use the set() method:

// Add a new document in collection "cities"
db.collection("cities").document("LA").setData([
   "name": "Los Angeles",
   "state": "CA",
   "country": "USA"
]) { err in
   if let err = err {
       print("Error writing document: (err)")
   } else {
       print("Document successfully written!")
   }
}

If the document does not exist, it will be created. If the document does exist, its contents will be overwritten with the newly provided data, unless you specify that the data should be merged into the existing document, as follows:

// Update one field, creating the document if it does not exist.
db.collection("cities").document("BJ").setData([ "capital": true ], merge: true)

If you’re not sure whether the document exists, pass the option to merge the new data with any existing document to avoid overwriting entire documents.

Data types

Cloud Firestore lets you write a variety of data types inside a document, including strings, booleans, numbers, dates, null, and nested arrays and objects. Cloud Firestore always stores numbers as doubles, regardless of what type of number you use in your code.

Custom objects

java로 만 가능하다.즉 android에서만 적용 가능

Add a document

When you use set() to create a document, you must specify an ID for the document to create. For example:

db.collection("cities").document("new-city-id").setData(data)

But sometimes there isn’t a meaningful ID for the document, and it’s more convenient to let Cloud Firestore auto-generate an ID for you. You can do this by calling add():

// Add a new document with a generated id.
var ref: DocumentReference? = nil
ref = db.collection("cities").addDocument(data: [
   "name": "Tokyo",
   "country": "Japan"
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

In some cases, it can be useful to create a document reference with an auto-generated ID, then use the reference later. For this use case, you can call doc():

let newCityRef = db.collection("cities").document()

// later...
newCityRef.setData([
   // ...
])

Behind the scenes, .add(...) and .doc().set(...) are completely equivalent, so you can use whichever is more convenient.

Update a document

To update some fields of a document without overwriting the entire document, use the update() method:

let washingtonRef = db.collection("cities").document("DC")

// Set the "capital" field of the city 'DC'
washingtonRef.updateData([
   "capital": true
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Update fields in nested objects

If your document contains nested objects, you can use “dot notation” to reference nested fields within the document when you call update():

// Create an initial document to update.
let frankDocRef = db.collection("users").document("frank")
frankDocRef.setData([
   "name": "Frank",
   "favorites": [ "food": "Pizza", "color": "Blue", "subject": "recess" ],
   "age": 12
   ])

// To update age and favorite color:
db.collection("users").document("frank").updateData([
   "age": 13,
   "favorites.color": "Red"
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

You can also add server timestamps to specific fields in your documents, to track when an update was received by the server:

db.collection("objects").document("some-id").updateData([
   "lastUpdated": FieldValue.serverTimestamp(),
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Update elements in an array

If your document contains an array field, you can use arrayUnion() and arrayRemove() to add and remove elements. arrayUnion() adds elements to an array but only elements not already present. arrayRemove() removes all instances of each given element.

let washingtonRef = db.collection("cities").document("DC")

// Atomically add a new region to the "regions" array field.
washingtonRef.updateData([
   "regions": FieldValue.arrayUnion(["greater_virginia"])
])

// Atomically remove a region from the "regions" array field.
washingtonRef.updateData([
   "regions": FieldValue.arrayRemove(["east_coast"])
])

Transactions & Batched Writes

https://firebase.google.com/docs/firestore/manage-data/transactions

Transactions and batched writes

  • Transactions: a transaction is a set of read and write operations on one or more documents.
  • Batched Writes: a batched write is a set of write operations on one or more documents.

Updating data with transactions

Using the Cloud Firestore client libraries, you can group multiple operations into a single transaction. Transactions are useful when you want to update a field’s value based on its current value, or the value of some other field. You could increment a counter by creating a transaction that reads the current value of the counter, increments it, and writes the new value to Cloud Firestore.

A transaction consists of any number of get() operations followed by any number of write operations such as set(),update(), or delete(). In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. This feature ensures that the transaction runs on up-to-date and consistent data.

Transactions never partially apply writes. All writes execute at the end of a successful transaction.

When using transactions, note that:

  • Read operations must come before write operations.
  • A function calling a transaction (transaction function) might run more than once if a concurrent edit affects a document that the transaction reads.
  • Transaction functions should not directly modify application state.
  • Transactions will fail when the client is offline.

The following example shows how to create and run a transaction:

let sfReference = db.collection("cities").document("SF")

db.runTransaction({ (transaction, errorPointer) -> Any? in
   let sfDocument: DocumentSnapshot
   do {
       try sfDocument = transaction.getDocument(sfReference)
   } catch let fetchError as NSError {
       errorPointer?.pointee = fetchError
       return nil
   }

   guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -1,
           userInfo: [
               NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot (sfDocument)"
           ]
       )
       errorPointer?.pointee = error
       return nil
   }

   transaction.updateData(["population": oldPopulation + 1], forDocument: sfReference)
   return nil
}) { (object, error) in
   if let error = error {
       print("Transaction failed: (error)")
   } else {
       print("Transaction successfully committed!")
   }
}

Passing information out of transactions

Do not modify application state inside of your transaction functions. Doing so will introduce concurrency issues, because transaction functions can run multiple times and are not guaranteed to run on the UI thread. Instead, pass information you need out of your transaction functions. The following example builds on the previous example to show how to pass information out of a transaction:

let sfReference = db.collection("cities").document("SF")

db.runTransaction({ (transaction, errorPointer) -> Any? in
   let sfDocument: DocumentSnapshot
   do {
       try sfDocument = transaction.getDocument(sfReference)
   } catch let fetchError as NSError {
       errorPointer?.pointee = fetchError
       return nil
   }

   guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -1,
           userInfo: [
               NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot (sfDocument)"
           ]
       )
       errorPointer?.pointee = error
       return nil
   }

   let newPopulation = oldPopulation + 1
   guard newPopulation <= 1000000 else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -2,
           userInfo: [NSLocalizedDescriptionKey: "Population (newPopulation) too big"]
       )
       errorPointer?.pointee = error
       return nil
   }

   transaction.updateData(["population": newPopulation], forDocument: sfReference)
   return newPopulation
}) { (object, error) in
   if let error = error {
       print("Error updating population: (error)")
   } else {
       print("Population increased to (object!)")
   }
}

Transaction failure

A transaction can fail for the following reasons:

  • The transaction contains read operations after write operations. Read operations must always come before any write operations.
  • The transaction read a document that was modified outside of the transaction. In this case, the transaction automatically runs again. The transaction is retried a finite number of times.

A failed transaction returns an error and does not write anything to the database. You do not need to roll back the transaction; Cloud Firestore does this automatically.

Batched writes

If you do not need to read any documents in your operation set, you can execute multiple write operations as a single batch that contains any combination of set(), update(), or delete() operations. A batch of writes completes atomically and can write to multiple documents.

Batched writes are also useful for migrating large data sets to Cloud Firestore. A batched write can contain up to 500 operations and batching operations together reduces connection overhead resulting in faster data migration.

Batched writes have fewer failure cases than transactions and use simpler code. They are not affected by contention issues, because they don’t depend on consistently reading any documents. Batched writes execute even when the user’s device is offline. The following example shows how to build and commit a batch of writes:

// Get new write batch
let batch = db.batch()

// Set the value of 'NYC'
let nycRef = db.collection("cities").document("NYC")
batch.setData([:], forDocument: nycRef)

// Update the population of 'SF'
let sfRef = db.collection("cities").document("SF")
batch.updateData(["population": 1000000 ], forDocument: sfRef)

// Delete the city 'LA'
let laRef = db.collection("cities").document("LA")
batch.deleteDocument(laRef)

// Commit the batch
batch.commit() { err in
   if let err = err {
       print("Error writing batch (err)")
   } else {
       print("Batch write succeeded.")
   }
}

Data validation for atomic operations

For mobile/web client libraries, you can validate data using Cloud Firestore Security Rules. You can ensure that related documents are always updated atomically and always as part of a transaction or batched write. Use the getAfter()security rule function to access and validate the state of a document after a set of operations completes but beforeCloud Firestore commits the operations.

For example, imagine that the database for the cities example also contains a countries collection. Each countrydocument uses a last_updated field to keep track of the last time any city related to that country was updated. The following security rules require that an update to a city document must also atomically update the related country’s last_updated field:

service cloud.firestore {
 match /databases/{database}/documents {
   // If you update a city doc, you must also
   // update the related country's last_updated field.
   match /cities/{city} {
     allow write: if request.auth.uid != null &&
       getAfter(
         /databases/$(database)/documents/countries/$(request.resource.data.country)
       ).data.last_updated == request.time;
   }

   match /countries/{country} {
     allow write: if request.auth.uid != null;
   }
 }
}

Security rules limits

In security rules for transactions or batched writes, there is a limit of 20 document access calls for the entire atomic operation in addition to the normal 10 call limit for each single document operation in the batch.

For example, consider the following rules for a chat application:

service cloud.firestore {
 match /databases/{db}/documents {
   function prefix() {
     return /databases/{db}/documents;
   }
   match /chatroom/{roomId} {
     allow read, write: if roomId in get(/$(prefix())/users/$(request.auth.uid)).data.chats
                           || exists(/$(prefix())/admins/$(request.auth.uid));
   }
   match /users/{userId} {
     allow read, write: if userId == request.auth.uid
                           || exists(/$(prefix())/admins/$(request.auth.uid));
   }
   match /admins/{userId} {
     allow read, write: if exists(/$(prefix())/admins/$(request.auth.uid));
   }
 }
}

The snippets below illustrate the number of document access calls used for a few data access patterns:

// 0 document access calls used, because the rules evaluation short-circuits
// before the exists() call is invoked.
db.collection('user').doc('myuid').get(...);

// 1 document access call used. The maximum total allowed for this call
// is 10, because it is a single document request.
db.collection('chatroom').doc('mygroup').get(...);

// Initializing a write batch...
var batch = db.batch();

// 2 document access calls used, 10 allowed.
var group1Ref = db.collection("chatroom").doc("group1");
batch.set(group1Ref, {msg: "Hello, from Admin!"});

// 1 document access call used, 10 allowed.
var newUserRef = db.collection("users").doc("newuser");
batch.update(newUserRef, {"lastSignedIn": new Date()});

// 1 document access call used, 10 allowed.
var removedAdminRef = db.collection("admin").doc("otheruser");
batch.delete(removedAdminRef);

// The batch used a total of 2 + 1 + 1 = 4 document access calls, out of a total
// 20 allowed.
batch.commit();

Delete data from Cloud Firestore

https://firebase.google.com/docs/firestore/manage-data/delete-data

delete() 를 이용한다.

db.collection("cities").document("DC").delete() { err in
   if let err = err {
       print("Error removing document: (err)")
   } else {
       print("Document successfully removed!")
   }
}

Warning: Deleting a document does not delete its subcollections!

각각 하부의 subcollection은 손수 삭제해야 한다.


Delete fields

To delete specific fields from a document, use the FieldValue.delete() method when you update a document:

db.collection("cities").document("BJ").updateData([
   "capital": FieldValue.delete(),
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Delete collections

To delete an entire collection or subcollection in Cloud Firestore, retrieve all the documents within the collection or subcollection and delete them. If you have larger collections, you may want to delete the documents in smaller batches to avoid out-of-memory errors. Repeat the process until you’ve deleted the entire collection or subcollection.

Deleting a collection requires coordinating an unbounded number of individual delete requests. If you need to delete entire collections, do so only from a trusted server environment. While it is possible to delete a collection from a mobile/web client, doing so has negative security and performance implications.

The snippets below are somewhat simplified and do not deal with error handling, security, deleting subcollections, or maximizing performance. To learn more about one recommended approach to deleting collections in production, seeDeleting Collections and Subcollections.

Delete data with the Firebase CLI

You can also use the Firebase CLI to delete documents and collections. Use the following command to delete data:

firebase firestore:delete [options] <<path>>

Get data

https://firebase.google.com/docs/firestore/query-data/get-data

There are two ways to retrieve data stored in Cloud Firestore.

  • Call a method to get the data.
  • Set a listener to receive data-change events.

Get a document

The following example shows how to retrieve the contents of a single document using get():


let docRef = db.collection("cities").document("SF")

docRef.getDocument { (document, error) in
   if let document = document, document.exists {
       let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
       print("Document data: (dataDescription)")
   } else {
       print("Document does not exist")
   }
}

Note: If there is no document at the location referenced by docRef, the resulting document will be empty and calling existson it will return false.

Source Options

For platforms with offline support, you can set the source option to control how a get call uses the offline cache.

By default, a get call will attempt to fetch the latest document snapshot from your database. On platforms with offline support, the client library will use the offline cache if the network is unavailable or if the request times out.

You can specify the source option in a get() call to change the default behavior. You can fetch from only the database and ignore the offline cache, or you can fetch from only the offline cache. For example:

let docRef = db.collection("cities").document("SF")

// Force the SDK to fetch the document from the cache. Could also specify
// FirestoreSource.server or FirestoreSource.default.
docRef.getDocument(source: .cache) { (document, error) in
 if let document = document {
   let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
   print("Cached document data: (dataDescription)")
 } else {
   print("Document does not exist in cache")
 }
}

Custom objects

The previous example retrieved the contents of the document as a map, but in some languages it’s often more convenient to use a custom object type. In Add Data, you defined a City class that you used to define each city. You can turn your document back into a City object:

let docRef = db.collection("cities").document("BJ")

docRef.getDocument { (document, error) in
   if let city = document.flatMap({
     $0.data().flatMap({ (data) in
       return City(dictionary: data)
     })
   }) {
       print("City: (city)")
   } else {
       print("Document does not exist")
   }
}

Important: Each custom class must have a public constructor that takes no arguments. In addition, the class must include a public getter for each property.

Get multiple documents from a collection

You can also retrieve multiple documents with one request by querying documents in a collection. For example, you can use where() to query for all of the documents that meet a certain condition, then use get() to retrieve the results:

db.collection("cities").whereField("capital", isEqualTo: true)
   .getDocuments() { (querySnapshot, err) in
       if let err = err {
           print("Error getting documents: (err)")
       } else {
           for document in querySnapshot!.documents {
               print("(document.documentID) => (document.data())")
           }
       }
}

By default, Cloud Firestore retrieves all documents that satisfy the query in ascending order by document ID, but you can order and limit the data returned.

Get all documents in a collection

In addition, you can retrieve all documents in a collection by omitting the where() filter entirely:

db.collection("cities").getDocuments() { (querySnapshot, err) in
   if let err = err {
       print("Error getting documents: (err)")
   } else {
       for document in querySnapshot!.documents {
           print("(document.documentID) => (document.data())")
       }
   }
}

List subcollections of a document

The getCollections() method of the Cloud Firestore server client libraries lists all subcollections of a document reference.

Retrieving a list of collections is not possible with the mobile/web client libraries. You should only look up collection names as part of administrative tasks in trusted server environments. If you find that you need this capability in the mobile/web client libraries, consider restructuring your data so that subcollection names are predictable.

Get real time updates

https://firebase.google.com/docs/firestore/query-data/listen

db.collection("cities").document("SF")
   .addSnapshotListener { documentSnapshot, error in
     guard let document = documentSnapshot else {
       print("Error fetching document: (error!)")
       return
     }
     guard let data = document.data() else {
       print("Document data was empty.")
       return
     }
     print("Current data: (data)")
   }

Events for local changes

Local writes in your app will invoke snapshot listeners immediately. This is because of an important feature called “latency compensation.” When you perform a write, your listeners will be notified with the new data before the data is sent to the backend.

Retrieved documents have a metadata.hasPendingWrites property that indicates whether the document has local changes that haven’t been written to the backend yet. You can use this property to determine the source of events received by your snapshot listener:

db.collection("cities").document("SF")
   .addSnapshotListener { documentSnapshot, error in
       guard let document = documentSnapshot else {
           print("Error fetching document: (error!)")
           return
       }
       let source = document.metadata.hasPendingWrites ? "Local" : "Server"
       print("(source) data: (document.data() ?? [:])")
   }


Events for metadata changes

// Listen to document metadata.
db.collection("cities").document("SF")
   .addSnapshotListener(includeMetadataChanges: true) { documentSnapshot, error in
       // ...
   }

ref) https://stackoverflow.com/a/50186785

Listen to multiple documents in a collection

db.collection("cities").whereField("state", isEqualTo: "CA")
   .addSnapshotListener { querySnapshot, error in
       guard let documents = querySnapshot?.documents else {
           print("Error fetching documents: (error!)")
           return
       }
       let cities = documents.map { $0["name"]! }
       print("Current cities in CA: (cities)")
   }

The snapshot handler will receive a new query snapshot every time the query results change (that is, when a document is added, removed, or modified).

Important: As explained above under

Events for local changes

, you will receive events immediately for your local writes. Your listener can use the

metadata.hasPendingWrites

field on each document to determine whether the document has local changes that have not yet been written to the backend.

View changes between snapshots

document에 어떤 변화( add, modify,remove )가 있었는지를 파악하는 방법

db.collection("cities").whereField("state", isEqualTo: "CA")
    .addSnapshotListener { querySnapshot, error in
        guard let snapshot = querySnapshot else {
            print("Error fetching snapshots: (error!)")
            return
        }
        snapshot.documentChanges.forEach { diff in
            if (diff.type == .added) {
                print("New city: (diff.document.data())")
            }
            if (diff.type == .modified) {
                print("Modified city: (diff.document.data())")
            }
            if (diff.type == .removed) {
                print("Removed city: (diff.document.data())")
            }
        }
    }

Detach a listener

listener를 사용후 필요하지 않는 경우 제거해주어야 한다.

let listener = db.collection("cities").addSnapshotListener { querySnapshot, error in
   // ...
}

// ...

// Stop listening to changes
listener.remove()

Perform simple and compound queries

https://firebase.google.com/docs/firestore/query-data/queries

collection으로 부터 특정 조건이 맞는 document를 찾아내거나 추리거나 하는 작업을 query라고 한다. 

Simple queries

// Create a reference to the cities collection
let citiesRef = db.collection("cities")

// Create a query against the collection.
let query = citiesRef.whereField("state", isEqualTo: "CA")
let capitalCities = db.collection("cities").whereField("capital", isEqualTo: true)
citiesRef.whereField("state", isEqualTo: "CA")
citiesRef.whereField("population", isLessThan: 100000)
citiesRef.whereField("name", isGreaterThanOrEqualTo: "San Francisco")

Array membership

citiesRef
 .whereField("regions", arrayContains: "west_coast")

Compound queries

You can also chain multiple where() methods to create more specific queries (logical AND). However, to combine the equality operator (==) with a range or array-contains clause (<, <=, >, >=, or array_contains), make sure to create a composite index.

citiesRef
   .whereField("state", isEqualTo: "CO")
   .whereField("name", isEqualTo: "Denver")
citiesRef
   .whereField("state", isEqualTo: "CA")
   .whereField("population", isLessThan: 1000000)

You can only perform range comparisons (<, <=, >, >=) on a single field, and you can include at most one array_contains clause in a compound query:

citiesRef
   .whereField("state", isGreaterThanOrEqualTo: "CA")
   .whereField("state", isLessThanOrEqualTo: "IN")
citiesRef
   .whereField("state", isEqualTo: "CA")
   .whereField("population", isGreaterThan: 1000000)


틀린예)

citiesRef
   .whereField("state", isGreaterThanOrEqualTo: "CA")
   .whereField("population", isGreaterThan: 1000000)

Query limitations

Cloud Firestore does not support the following types of queries:

  • Queries with range filters on different fields, as described in the previous section.
  • Single queries across multiple collections or subcollections. Each query runs against a single collection of documents. For more information about how your data structure affects your queries, see Choose a Data Structure.
  • Logical OR queries. In this case, you should create a separate query for each OR condition and merge the query results in your app.
  • Queries with a != clause. In this case, you should split the query into a greater-than query and a less-than query. For example, although the query clause where("age", "!=", "30") is not supported, you can get the same result set by combining two queries, one with the clause where("age", "<", "30") and one with the clause where("age", ">", 30).

Order and limit data

https://firebase.google.com/docs/firestore/query-data/order-limit-data

Order and limit data

citiesRef.order(by: "name").limit(to: 3)
citiesRef.order(by: "name", descending: true).limit(to: 3)
citiesRef
   .order(by: "state")
   .order(by: "population", descending: true)

You can combine where() filters with orderBy() and limit(). In the following example, the queries define a population threshold, sort by population in ascending order, and return only the first few results that exceed the threshold:

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "population")
   .limit(to: 2)

However, if you have a filter with a range comparison (<, <=, >, >=), your first ordering must be on the same field:

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "population")

틀린예)

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "country")

Pagination

https://firebase.google.com/docs/firestore/query-data/query-cursors

Add a simple cursor to a query

// Get all cities with population over one million, ordered by population.
db.collection("cities")
   .order(by: "population")
   .start(at: [1000000])
// Get all cities with population less than one million, ordered by population.
db.collection("cities")
   .order(by: "population")
   .end(at: [1000000])

Use a document snapshot to define the query cursor

document를 cursor로 직접 이용하는 경우 ( start()에 직접 document obj를 pass 한다 )

db.collection("cities")
   .document("SF")
   .addSnapshotListener { (document, error) in
       guard let document = document else {
           print("Error retreving cities: (error.debugDescription)")
           return
       }

       // Get all cities with a population greater than or equal to San Francisco.
       let sfSizeOrBigger = db.collection("cities")
           .order(by: "population")
           .start(atDocument: document)
}

Paginate a query

// Construct query for first 25 cities, ordered by population
let first = db.collection("cities")
   .order(by: "population")
   .limit(to: 25)

first.addSnapshotListener { (snapshot, error) in
   guard let snapshot = snapshot else {
       print("Error retreving cities: (error.debugDescription)")
       return
   }

   guard let lastSnapshot = snapshot.documents.last else {
       // The collection is empty.
       return
   }

   // Construct a new query starting after this document,
   // retrieving the next 25 cities.
   let next = db.collection("cities")
       .order(by: "population")
       .start(afterDocument: lastSnapshot)

   // Use the query for pagination.
   // ...
}

Set multiple cursor conditions

조건에 해당하는 document가 여러개 인경우 조건을 좀더 명확하게 지정할수 있다.

// Will return all Springfields
db.collection("cities")
   .order(by: "name")
   .order(by: "state")
   .start(at: ["Springfield"])

// Will return "Springfield, Missouri" and "Springfield, Wisconsin"
db.collection("cities")
   .order(by: "name")
   .order(by: "state")
   .start(at: ["Springfield", "Missouri"])

index

https://firebase.google.com/docs/firestore/query-data/index-overview

Index types

Cloud Firestore uses two types of indexes, single-field and composite. Both types of indexes are uniquely defined by the fields they index and the index mode on each field.

Automatic indexing

By default, Cloud Firestore automatically maintains an index for each field in a document and each subfield in a map. Cloud Firestore uses the following settings for automatically created single-field indexes:

  • For each non-array and non-map field, Cloud Firestore defines two single-field indexes, one in ascending mode and one in descending mode.
  • For each map field, Cloud Firestore creates one ascending index and one descending index for each non-array and non-map subfield in the map.
  • For each array field in a document, Cloud Firestore creates and maintains an array-contains index.

If you need to run a compound query that uses a range comparison (<, <=, >, or >=) or if you need to sort by a different field, you must create a composite index for that query.


Composite indexes

A composite index stores a sorted mapping of all the documents in a collection that contain multiple specific fields instead of just one. A composite index also defines an index mode for each of its fields (ascending, descending, or array-contains), and the index is sorted based on the index modes.

Note: You can have at most one array field per composite index.



Security Rules

https://firebase.google.com/docs/firestore/security/get-started

Note: The server client libraries bypass all Cloud Firestore Security Rules and instead authenticate through Google Application Default Credentials. If you are using the server client libraries or the REST or RPC APIs, make sure to set up Cloud Identity and Access Management for Cloud Firestore.

server에서 접근하는 연결의 security rule은 IAM으로 한다.

예)

// Allow read/write access on all documents to any user signed in to the application
service cloud.firestore {
 match /databases/{database}/documents {
   match /{document=**} {
     allow read, write: if request.auth.uid != null;
   }
 }
}

CLI로 rule을 수정하는 경우 주의사항

Note: When you deploy security rules using the Firebase CLI, the rules defined in your project directory overwrite any existing rules in the Firebase console. So, if you choose to define or edit your security rules using the Firebase console, make sure that you also update the rules defined in your project directory.

Structuring Cloud Firestore Security Rules

https://firebase.google.com/docs/firestore/security/rules-structure

Service and database declaration

Cloud Firestore Security Rules always begin with the following declaration:

service cloud.firestore {
 match /databases/{database}/documents {
   // ...
 }
}

Basic read/write rules

service cloud.firestore {
 match /databases/{database}/documents {

   // Match any document in the 'cities' collection
   match /cities/{city} {
     allow read: if <condition>;
     allow write: if <condition>;
   }
 }
}

In the example above, the match statement uses the {city} wildcard syntax. This means the rule applies to any document in the cities collection, such as /cities/SF or /cities/NYC. When the allow expressions in the match statement are evaluated, the city variable will resolve to the city document name, such as SF or NYC.

Granular operations

service cloud.firestore {
 match /databases/{database}/documents {
   // A read rule can be divided into get and list rules
   match /cities/{city} {
     // Applies to single document read requests
     allow get: if <condition>;

     // Applies to queries and collection read requests
     allow list: if <condition>;
   }

   // A write rule can be divided into create, update, and delete rules
   match /cities/{city} {
     // Applies to writes to nonexistent documents
     allow create: if <condition>;

     // Applies to writes to existing documents
     allow update: if <condition>;

     // Applies to delete operations
     allow delete: if <condition>;
   }
 }
}

Hierarchical data

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     allow read, write: if <condition>;

       // Explicitly define rules for the 'landmarks' subcollection
       match /landmarks/{landmark} {
         allow read, write: if <condition>;
       }
   }
 }
}

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     match /landmarks/{landmark} {
       allow read, write: if <condition>;
     }
   }
 }
}

위와 아래는 같은 내용

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city}/landmarks/{landmark} {
     allow read, write: if <condition>;
   }
 }
}

If you want rules to apply to an arbitrarily deep hierarchy, use the recursive wildcard syntax, {name=**}:

service cloud.firestore {
 match /databases/{database}/documents {
   // Matches any document in the cities collection as well as any document
   // in a subcollection.
   match /cities/{document=**} {
     allow read, write: if <condition>;
   }
 }
}

When using the recursive wildcard syntax, the wildcard variable will contain the entire matching path segment, even if the document is located in a deeply nested subcollection. For example, the rules listed above would match a document located at /cities/SF/landmarks/coit_tower, and the value of the document variable would be SF/landmarks/coit_tower.

Recursive wildcards cannot match an empty path, so match /cities/{city}/{document=**} will match documents in subcollections but not in the cities collection, whereas match /cities/{document=**} will match both documents in the cities collection and subcollections.

It’s possible for a document to match more than one match statement. In the case where multiple allow expressions match a request, the access is allowed if any of the conditions is true:

service cloud.firestore {
 match /databases/{database}/documents {
   // Matches any document in the 'cities' collection.
   match /cities/{city} {
     allow read, write: if false;
   }

   // Matches any document in the 'cities' collection or subcollections.
   match /cities/{document=**} {
     allow read, write: if true;
   }
 }
}

In the example above, all reads and writes to the cities collection will be allowed because the second rule is always true, even though the first rule is always false.

firestore에서 사용되는 class reference doc

ref) https://firebase.google.com/docs/reference/security/database/

firestore security rules설정 예시

https://www.youtube.com/watch?v=b7PUm7LmAOw

Writing conditions for Cloud Firestore Security Rules

https://firebase.google.com/docs/firestore/security/rules-conditions

Authentication

One of the most common security rule patterns is controlling access based on the user’s authentication state. For example, your app may want to allow only signed-in users to write data:

service cloud.firestore {
 match /databases/{database}/documents {
   // Allow the user to access documents in the "cities" collection
   // only if they are authenticated.
   match /cities/{city} {
     allow read, write: if request.auth.uid != null;
   }
 }
}

자세한 Request class 내용

https://firebase.google.com/docs/reference/rules/rules.firestore.Request

If your app uses Firebase Authentication, the request.auth variable contains the authentication information for the client requesting data. For more information about request.auth, see the reference documentation.

service cloud.firestore {
 match /databases/{database}/documents {
   // Make sure the uid of the requesting user matches name of the user
   // document. The wildcard expression {userId} makes the userId variable
   // available in rules.
   match /users/{userId} {
     allow read, update, delete: if request.auth.uid == userId;
     allow create: if request.auth.uid != null;
   }
 }
}

Data validation

Many apps store access control information as fields on documents in the database. Cloud Firestore Security Rules can dynamically allow or deny access based on document data:

service cloud.firestore {
 match /databases/{database}/documents {
   // Allow the user to read data if the document has the 'visibility'
   // field set to 'public'
   match /cities/{city} {
     allow read: if resource.data.visibility == 'public';
   }
 }
}

The resource variable refers to the requested document, and resource.data is a map of all of the fields and values stored in the document. For more information on the resource variable, see the reference documentation.

When writing data, you may want to compare incoming data to existing data. In this case, if your ruleset allows the pending write, the request.resource variable contains the future state of the document. For update operations that only modify a subset of the document fields, the request.resource variable will contain the pending document state after the operation. You can check the field values in request.resource to prevent unwanted or inconsistent data updates:

service cloud.firestore {
 match /databases/{database}/documents {
   // Make sure all cities have a positive population and
   // the name is not changed
   match /cities/{city} {
     allow update: if request.resource.data.population > 0
                   && request.resource.data.name == resource.data.name;
   }
 }
}

Access other documents

Using the get() and exists() functions, your security rules can evaluate incoming requests against other documents in the database. The get() and exists() functions both expect fully specified document paths. When using variables to construct paths for get() and exists(), you need to explicitly escape variables using the $(variable) syntax.

In the example below, the database variable is captured by the match statement match /databases/{database}/documents and used to form the path:

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     // Make sure a 'users' document exists for the requesting user before
     // allowing any writes to the 'cities' collection
     allow create: if exists(/databases/$(database)/documents/users/$(request.auth.uid))

     // Allow the user to delete cities if their user document has the
     // 'admin' field set to 'true'
     allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
   }
 }
}

For writes, you can use the getAfter() function to access the state of a document after a transaction or batch of writes completes but before the transaction or batch commits. Like get(), the getAfter() function takes a fully specified document path. You can use getAfter() to define sets of writes that must take place together as a transaction or batch.

Custom functions

  • Functions can contain only a single return statement. They cannot contain any additional logic. For example, they cannot create intermediate variables, execute loops, or call external services.
  • Functions can automatically access functions and variables from the scope in which they are defined. For example, a function defined within the service cloud.firestore scope has access to the resource variable and built-in functions such as get() and exists().
  • Functions may call other functions but may not recurse. The total call stack depth is limited to 10.

A function is defined with the function keyword and takes zero or more arguments. For example, you may want to combine the two types of conditions used in the examples above into a single function:

service cloud.firestore {
 match /databases/{database}/documents {
   // True if the user is signed in or the requested data is 'public'
   function signedInOrPublic() {
     return request.auth.uid != null || resource.data.visibility == 'public';
   }

   match /cities/{city} {
     allow read, write: if signedInOrPublic();
   }

   match /users/{user} {
     allow read, write: if signedInOrPublic();
   }
 }
}

Securely query data

https://firebase.google.com/docs/firestore/security/rules-query

Queries and security rules

When writing queries to retrieve documents, keep in mind that security rules are not filters—queries are all or nothing. To save you time and resources, Cloud Firestore evaluates a query against its potential result set instead of the actual field values for all of your documents. If a query could potentially return documents that the client does not have permission to read, the entire request fails.

Note: This behavior applies to queries that retrieve one or more documents from a collection and not to individual document retrievals. When you use a document ID to retrieve a single document, Cloud Firestore reads the document and evaluates the request using your security rules and the actual document properties.

As the examples below demonstrate, you must write your queries to fit the constraints of your security rules.


Secure and query documents based on

auth.uid

The following example demonstrates how to write a query to retrieve documents protected by a security rule. Consider a database that contains a collection of story documents:

/stories/{storyid}

{
 title: "A Great Story",
 content: "Once upon a time...",
 author: "some_auth_id",
 published: false
}

In addition to the title and content fields, each document stores the author and published fields to use for access control. These examples assume the app uses Firebase Authentication to set the author field to the UID of the user who created the document. Firebase Authentication also populates the request.auth variable in the security rules.

The following security rule uses the request.auth and resource.data variables to restrict read and write access for each story to its author:

service cloud.firestore {
 match /databases/{database}/documents {
   match /stories/{storyid} {
     // Only the authenticated user who authored the document can read or write
     allow read, write: if request.auth.uid == resource.data.author;
   }
 }
}

Suppose that your app includes a page that shows the user a list of story documents that they authored. You might expect that you could use the following query to populate this page. However, this query will fail, because it does not include the same constraints as your security rules:

Invalid: Query constraints do not match security rules constraints

// This query will fail
db.collection("stories").get()

The query fails even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database. If a query could potentially include documents that violate your security rules, the query will fail.

In contrast, the following query succeeds, because it includes the same constraint on the author field as the security rules:

Valid: Query constraints match security rules constraints

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

Secure and query documents based on a field

To further demonstrate the interaction between queries and rules, the security rules below expand read access for the stories collection to allow any user to read story documents where the published field is set to true.

service cloud.firestore {
 match /databases/{database}/documents {
   match /stories/{storyid} {
     // Anyone can read a published story; only story authors can read unpublished stories
     allow read: if resource.data.published == true || request.auth.uid == resource.data.author;
     // Only story authors can write
     allow write: if request.auth.uid == resource.data.author;
   }
 }
}

The query for published pages must include the same constraints as the security rules:

db.collection("stories").where("published", "==", true).get()

The query constraint .where("published", "==", true) guarantees that resource.data.published is true for any result. Therefore, this query satisfies the security rules and is allowed to read data.

Evaluating constraints on queries

Your security rules can also accept or deny queries based on their constraints. The request.query variable contains the limit, offset, and orderBy properties of a query. For example, your security rules can deny any query that doesn’t limit the maximum number of documents retrieved to a certain range:

allow list: if request.query.limit <= 10;

Note:

You can break read rules into get and list rules

. Rules for

get

apply to requests for single documents, and rules for

list

apply to queries and requests for collections.

The following ruleset demonstrates how to write security rules that evaluate constraints placed on queries. This example expands the previous stories ruleset with the following changes:

  • The ruleset separates the read rule into rules for get and list.
  • The get rule restricts retrieval of single documents to public documents or documents the user authored.
  • The list rule applies the same restrictions as get but for queries. It also checks the query limit, then denies any query without a lmit or with a limit greater than 10.
  • The ruleset defines an authorOrPublished() function to avoid code duplication.
service cloud.firestore {

 match /databases/{database}/documents {

   match /stories/{storyid} {

     // Returns `true` if the requested story is 'published'
     // or the user authored the story
     function authorOrPublished() {
       return resource.data.published == true || request.auth.uid == resource.data.author;
     }

     // Deny any query not limited to 10 or fewer documents
     // Anyone can query published stories
     // Authors can query their unpublished stories
     allow list: if request.query.limit <= 10 &&
                    authorOrPublished();

     // Anyone can retrieve a published story
     // Only a story's author can retrieve an unpublished story
     allow get: if authorOrPublished();

     // Only a story's authors can write to a story
     allow write: if request.auth.uid == resource.data.author;
   }

 }
}

Enable offline data

https://firebase.google.com/docs/firestore/manage-data/enable-offline

Note: Offline persistence is supported only in Android, iOS, and web apps.

  • For Android and iOS, offline persistence is enabled by default. To disable persistence, set the PersistenceEnabled option to false.
  • For the web, offline persistence is disabled by default.
let settings = FirestoreSettings()
settings.isPersistenceEnabled = true

// Any additional options
// ...

// Enable offline data persistence
let db = Firestore.firestore()
db.settings = settings

Listen to offline data

While the device is offline, if you have enabled offline persistence, your listeners will receive listen events when the locally cached data changes. You can listen to documents, collections, and queries.

To check whether you’re receiving data from the server or the cache, use the fromCache property on the SnapshotMetadata in your snapshot event. If fromCache is true, the data came from the cache and might be stale or incomplete. If fromCache is false, the data is complete and current with the latest updates on the server.

By default, no event is raised if only the SnapshotMetadata changed. If you rely on the fromCache values, specify the includeMetadataChanges listen option when you attach your listen handler.

// Listen to metadata updates to receive a server snapshot even if
// the data is the same as the cached data.
db.collection("cities").whereField("state", isEqualTo: "CA")
   .addSnapshotListener(includeMetadataChanges: true) { querySnapshot, error in
       guard let snapshot = querySnapshot else {
           print("Error retreiving snapshot: (error!)")
           return
       }

       for diff in snapshot.documentChanges {
           if diff.type == .added {
               print("New city: (diff.document.data())")
           }
       }

       let source = snapshot.metadata.isFromCache ? "local cache" : "server"
       print("Metadata: Data fetched from (source)")
}

Get offline data

If you get a document while the device is offline, Cloud Firestore returns data from the cache. If the cache does not contain data for that document, or the document does not exist, the get call returns an error.

Query offline data

Querying works with offline persistence. You can retrieve the results of queries with either a direct get or by listening, as described in the preceding sections. You can also create new queries on locally persisted data while the device is offline, but the queries will initially run only against the cached documents.

Disable and enable network access

You can use method below to disable network access for your Cloud Firestore client. While network access is disabled, all snapshot listeners and document requests retrieve results from the cache. Write operations are queued until network access is re-enabled.

Firestore.firestore().disableNetwork { (error) in
   // Do offline things
   // ...
}

Use the following method to re-enable network access:

Firestore.firestore().enableNetwork { (error) in
   // Do online things
   // ...
}

스텝바이 스텝 노트

https://codelabs.developers.google.com/codelabs/firestore-ios/#0

실전 퀵 치트노트

https://firebaseopensource.com/projects/firebase/quickstart-ios/

.

Generics

For example, Swift’s Array and Dictionary types are both generic collections. You can create an array that holds Int values, or an array that holds String values, or indeed an array for any other type that can be created in Swift. Similarly, you can create a dictionary to store values of any specified type, and there are no limitations on what that type can be.

The Problem That Generics Solve

Here’s a standard, nongeneric function called swapTwoInts(_:_:), which swaps two Int values:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
   let temporaryA = a
   a = b
   b = temporaryA
}

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print(“someInt is now (someInt), and anotherInt is now (anotherInt)”)
// Prints “someInt is now 107, and anotherInt is now 3”

The swapTwoInts(_:_:) function is useful, but it can only be used with Int values. If you want to swap two String values, or two Double values, you have to write more functions, such as the swapTwoStrings(_:_:) and swapTwoDoubles(_:_:) functions shown below:

func swapTwoStrings(_ a: inout String, _ b: inout String) {
   let temporaryA = a
   a = b
   b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
   let temporaryA = a
   a = b
   b = temporaryA
}

It’s more useful, and considerably more flexible, to write a single function that swaps two values of any type. Generic code enables you to write such a function. (A generic version of these functions is defined below.)

Generic Functions

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
   let temporaryA = a
   a = b
   b = temporaryA
}

  1. func swapTwoInts(_ a: inout Int, _ b: inout Int)
  2. func swapTwoValues<T>(_ a: inout T, _ b: inout T)

The generic version of the function uses a placeholder type name (called T, in this case) instead of an actual type name (such as Int, String, or Double). The placeholder type name doesn’t say anything about what must be, but it does say that both a and b must be of the same type T, whatever T represents. The actual type to use in place of T is determined each time the swapTwoValues(_:_:) function is called.

var someInt = 3

var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3
var someString = “hello”
var anotherString = “world”
swapTwoValues(&someString, &anotherString)
// someString is now “world”, and anotherString is now “hello”

Type Parameters

In the swapTwoValues(_:_:) example above, the placeholder type T is an example of a type parameter. Type parameters specify and name a placeholder type, and are written immediately after the function’s name, between a pair of matching angle brackets (such as <T>).

Once you specify a type parameter, you can use it to define the type of a function’s parameters (such as the a and b parameters of the swapTwoValues(_:_:) function), or as the function’s return type, or as a type annotation within the body of the function. 

You can provide more than one type parameter by writing multiple type parameter names within the angle brackets, separated by commas.

Naming Type Parameters

In most cases, type parameters have descriptive names, such as Key and Value in Dictionary<Key, Value>and Element in Array<Element>, which tells the reader about the relationship between the type parameter and the generic type or function it’s used in. However, when there isn’t a meaningful relationship between them, it’s traditional to name them using single letters such as T, U, and V.

NOTE

Always give type parameters upper camel case names (such as T and MyTypeParameter

Generic Types

In addition to generic functions, Swift enables you to define your own generic types. These are custom classes, structures, and enumerations that can work with any type, in a similar way to Array and Dictionary.

The illustration below shows the push and pop behavior for a stack:

  1. There are currently three values on the stack.
  2. A fourth value is pushed onto the top of the stack.
  3. The stack now holds four values, with the most recent one at the top.
  4. The top item in the stack is popped.
  5. After popping a value, the stack once again holds three values.

Here’s how to write a nongeneric version of a stack, in this case for a stack of Int values:

struct IntStack {
   var items = [Int]()
   mutating func push(_ item: Int) {
       items.append(item)
   }
   mutating func pop() -> Int {
       return items.removeLast()
   }
}

This structure uses an Array property called items to store the values in the stack. Stack provides two methods, push and pop, to push and pop values on and off the stack. These methods are marked as mutating, because they need to modify (or mutate) the structure’s items array.

The IntStack type shown above can only be used with Int values, however. It would be much more useful to define a generic Stack class, that can manage a stack of any type of value.

Here’s a generic version of the same code:

struct Stack<Element> {
   var items = [Element]()
   mutating func push(_ item: Element) {
       items.append(item)
   }
   mutating func pop() -> Element {
       return items.removeLast()
   }
}

var stackOfStrings = Stack<String>()
stackOfStrings.push(“uno”)
stackOfStrings.push(“dos”)
stackOfStrings.push(“tres”)
stackOfStrings.push(“cuatro”)
// the stack now contains 4 strings

let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to “cuatro”, and the stack now contains 3 strings

Extending a Generic Type

When you extend a generic type, you don’t provide a type parameter list as part of the extension’s definition. Instead, the type parameter list from the original type definition is available within the body of the extension, and the original type parameter names are used to refer to the type parameters from the original definition.

(예를 들어 아래 예시에서 Element가 type parameter 이며 이것이 extending하는 부분에서 접근가능하다는 이야기)

struct Stack<Element> {   
}

extension Stack {

 // Element가 extended 정의의 type parameter이므로 접근 가능
   var topItem: Element? {
       return items.isEmpty ? nil : items[items.count – 1]
   }
}

if let topItem = stackOfStrings.topItem {
   print(“The top item on the stack is (topItem).”)
}
// Prints “The top item on the stack is tres.”

Type Constraints

Type constraints specify that a type parameter must inherit from a specific class, or conform to a particular protocol or protocol composition.

For example, Swift’s Dictionary type places a limitation on the types that can be used as keys for a dictionary. As described in Dictionaries, the type of a dictionary’s keys must be hashable. That is, it must provide a way to make itself uniquely representable. Dictionary needs its keys to be hashable so that it can check whether it already contains a value for a particular key. Without this requirement, Dictionary could not tell whether it should insert or replace a value for a particular key, nor would it be able to find a value for a given key that is already in the dictionary.

This requirement is enforced by a type constraint on the key type for Dictionary, which specifies that the key type must conform to the Hashable protocol, a special protocol defined in the Swift standard library. All of Swift’s basic types (such as String, Int, Double, and Bool) are hashable by default.

You can define your own type constraints when creating custom generic types, and these constraints provide much of the power of generic programming. Abstract concepts like Hashable characterize types in terms of their conceptual characteristics, rather than their concrete type.

Type Constraint Syntax

You write type constraints by placing a single class or protocol constraint after a type parameter’s name, separated by a colon, as part of the type parameter list. The basic syntax for type constraints on a generic function is shown below (although the syntax is the same for generic types):

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
   // function body goes here
}

The hypothetical function above has two type parameters. The first type parameter, T, has a type constraint that requires T to be a subclass of SomeClass. The second type parameter, U, has a type constraint that requires U to conform to the protocol SomeProtocol.

Type Constraints in Action

type constaints의 사용 예시

Here’s a nongeneric function called findIndex(ofString:in:), which is given a String value to find and an array of String values within which to find it. The findIndex(ofString:in:) function returns an optional Intvalue, which will be the index of the first matching string in the array if it’s found, or nil if the string can’t be found:

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
   for (index, value) in array.enumerated() {
       if value == valueToFind {
           return index
       }
   }
   return nil
}

The findIndex(ofString:in:) function can be used to find a string value in an array of strings:

let strings = [“cat”, “dog”, “llama”, “parakeet”, “terrapin”]
if let foundIndex = findIndex(ofString: “llama”, in: strings) {
   print(“The index of llama is (foundIndex)”)
}
// Prints “The index of llama is 2”

The principle of finding the index of a value in an array isn’t useful only for strings, however. You can write the same functionality as a generic function by replacing any mention of strings with values of some type Tinstead.

Here’s how you might expect a generic version of findIndex(ofString:in:), called findIndex(of:in:), to be written. Note that the return type of this function is still Int?, because the function returns an optional index number, not an optional value from the array. Be warned, though—this function doesn’t compile, for reasons explained after the example:

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
   for (index, value) in array.enumerated() {
       if value == valueToFind {
           return index
       }
   }
   return nil
}

This function doesn’t compile as written above. The problem lies with the equality check, “if value == valueToFind”. Not every type in Swift can be compared with the equal to operator (==). If you create your own class or structure to represent a complex data model, for example, then the meaning of “equal to” for that class or structure isn’t something that Swift can guess for you. Because of this, it isn’t possible to guarantee that this code will work for every possible type T, and an appropriate error is reported when you try to compile the code.

All is not lost, however. The Swift standard library defines a protocol called Equatable, which requires any conforming type to implement the equal to operator (==) and the not equal to operator (!=) to compare any two values of that type. All of Swift’s standard types automatically support the Equatable protocol.

Any type that is Equatable can be used safely with the findIndex(of:in:) function, because it’s guaranteed to support the equal to operator. To express this fact, you write a type constraint of Equatable as part of the type parameter’s definition when you define the function:

func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
   for (index, value) in array.enumerated() {
       if value == valueToFind {
           return index
       }
   }
   return nil
}

The single type parameter for findIndex(of:in:) is written as T: Equatable, which means “any type T that conforms to the Equatable protocol.”

The findIndex(of:in:) function now compiles successfully and can be used with any type that is Equatable, such as Double or String:

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex is an optional Int with no value, because 9.3 isn’t in the array
let stringIndex = findIndex(of: “Andrea”, in: [“Mike”, “Malcolm”, “Andrea”])
// stringIndex is an optional Int containing a value of 2

Associated Types

참고 사항) protocol에서 generic을 사용해야 하는 경우는 위에서 사용한 방법들과는 다르다. <T>의 방법은 사용 불가능하고 associatedtype을 대신 사용한다. 그 이전에는 typealias를 이용했으나 deprecated되었다.

참고 사항) 

generic protocols with associated type  https://blog.bobthedeveloper.io/generic-protocols-with-associated-type-7e2b6e079ee2

When defining a protocol, it’s sometimes useful to declare one or more associated types as part of the protocol’s definition. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. Associated types are specified with the associatedtype keyword.

Associated Types in Action

Here’s an example of a protocol called Container, which declares an associated type called Item:

protocol Container {
   associatedtype Item
   mutating func append(_ item: Item)
   var count: Int { get }
   subscript(i: Int) -> Item { get }
}

The Container protocol defines three required capabilities that any container must provide:

  • It must be possible to add a new item to the container with an append(_:) method.
  • It must be possible to access a count of the items in the container through a count property that returns an Int value.
  • It must be possible to retrieve each item in the container with a subscript that takes an Int index value.

This protocol doesn’t specify how the items in the container should be stored or what type they’re allowed to be. The protocol only specifies the three bits of functionality that any type must provide in order to be considered a Container. A conforming type can provide additional functionality, as long as it satisfies these three requirements.

Any type that conforms to the Container protocol must be able to specify the type of values it stores. Specifically, it must ensure that only items of the right type are added to the container, and it must be clear about the type of the items returned by its subscript.

To define these requirements, the Container protocol needs a way to refer to the type of the elements that a container will hold, without knowing what that type is for a specific container. The Container protocol needs to specify that any value passed to the append(_:) method must have the same type as the container’s element type, and that the value returned by the container’s subscript will be of the same type as the container’s element type.

To achieve this, the Container protocol declares an associated type called Item, written as associatedtype Item. The protocol doesn’t define what Item is—that information is left for any conforming type to provide.(generic type은 protocol 에서 정의되는 것이 아니고 conform하는 부분에서 수행된다.) Nonetheless, the Item alias provides a way to refer to the type of the items in a Container, and to define a type for use with the append(_:) method and subscript, to ensure that the expected behavior of any Container is enforced.

Here’s a version of the nongeneric IntStack type from earlier, adapted to conform to the Container protocol:

struct IntStack: Container {
   // original IntStack implementation
   var items = [Int]()
   mutating func push(_ item: Int) {
       items.append(item)
   }
   mutating func pop() -> Int {
       return items.removeLast()
   }

   // conformance to the Container protocol

  // 아래에서 설명하겠지만 이렇게 명시적으로 하는 방법이 있지만 암묵적으로도가능하다. 
   typealias Item = Int

   mutating func append(_ item: Int) {
       self.push(item)
   }
   var count: Int {
       return items.count
   }
   subscript(i: Int) -> Int {
       return items[i]
   }
}

The IntStack type implements all three of the Container protocol’s requirements, and in each case wraps part of the IntStack type’s existing functionality to satisfy these requirements.

Moreover, IntStack specifies that for this implementation of Container, the appropriate Item to use is a type of Int. The definition of typealias Item = Int turns the abstract type of Item into a concrete type of Int for this implementation of the Container protocol.

Thanks to Swift’s type inference, you don’t actually need to declare a concrete Item of Int as part of the definition of IntStack. Because IntStack conforms to all of the requirements of the Container protocol, Swift can infer the appropriate Item to use, simply by looking at the type of the append(_:) method’s itemparameter and the return type of the subscript. Indeed, if you delete the typealias Item = Int line from the code above, everything still works, because it’s clear what type should be used for Item. (명시적으로    typealias Item = Int 이렇게 하지 않아도 암묵적으로 swift가 알맞은 type을 찾을수 있다.)  

You can also make the generic Stack type conform to the Container protocol:

struct Stack<Element>: Container {
   // original Stack<Element> implementation
   var items = [Element]()
   mutating func push(_ item: Element) {
       items.append(item)
   }
   mutating func pop() -> Element {
       return items.removeLast()
   }
   // conformance to the Container protocol
   mutating func append(_ item: Element) {
       self.push(item)
   }
   var count: Int {
       return items.count
   }
   subscript(i: Int) -> Element {
       return items[i]
   }
}

This time, the type parameter Element is used as the type of the append(_:) method’s item parameter and the return type of the subscript. Swift can therefore infer that Element is the appropriate type to use as the Item for this particular container.

Extending an Existing Type to Specify an Associated Type

You can extend an existing type to add conformance to a protocol, as described in Adding Protocol Conformance with an Extension. This includes a protocol with an associated type.

Swift’s Array type already provides an append(_:) method, a count property, and a subscript with an Int index to retrieve its elements. These three capabilities match the requirements of the Container protocol. This means that you can extend Array to conform to the Container protocol simply by declaring that Array adopts the protocol. You do this with an empty extension, as described in Declaring Protocol Adoption with an Extension:

extension Array: Container {}

Array’s existing append(_:) method and subscript enable Swift to infer the appropriate type to use for Item, just as for the generic Stack type above. After defining this extension, you can use any Array as a Container.

Using Type Annotations to Constrain an Associated Type

You can add a type annotation to an associated type in a protocol, to require that conforming types satisfy the constraints described by the type annotation. For example, the following code defines a version of Container that requires the items in the container to be equatable.

protocol Container {

   // Item 은 Equatable protocol을 conform하는 것이어야 한다.
   associatedtype Item: Equatable
   mutating func append(_ item: Item)
   var count: Int { get }
   subscript(i: Int) -> Item { get }
}

To conform to this version of Container, the container’s Item type has to conform to the Equatable protocol.

Using Type Annotations to Constrain an Associated Type

You can add a type annotation to an associated type in a protocol, to require that conforming types satisfy the constraints described by the type annotation. For example, the following code defines a version of Container that requires the items in the container to be equatable.

protocol Container {
   associatedtype Item: Equatable
   mutating func append(_ item: Item)
   var count: Int { get }
   subscript(i: Int) -> Item { get }
}

To conform to this version of Container, the container’s Item type has to conform to the Equatable protocol.

Using a Protocol in Its Associated Type’s Constraints

(이해 못했던 부분)

A protocol can appear as part of its own requirements. For example, here’s a protocol that refines the Container protocol, adding the requirement of a suffix(_:) method. The suffix(_:) method returns a given number of elements from the end of the container, storing them in an instance of the Suffix type.

protocol SuffixableContainer: Container {
   associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
   func suffix(_ size: Int) -> Suffix
}

In this protocol, Suffix is an associated type, like the Item type in the Container example above. Suffix has two constraints: It must conform to the SuffixableContainer protocol (the protocol currently being defined), and its Item type must be the same as the container’s Item type. The constraint on Item is a generic whereclause, which is discussed in Associated Types with a Generic Where Clause below.

Here’s an extension of the Stack type from earlier that adds conformance to the SuffixableContainerprotocol:

extension Stack: SuffixableContainer {
   func suffix(_ size: Int) -> Stack {
       var result = Stack()
       for index in (count-size)..<count {
           result.append(self[index])
       }
       return result
   }
   // Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30

In the example above, the Suffix associated type for Stack is also Stack, so the suffix operation on Stackreturns another Stack. Alternatively, a type that conforms to SuffixableContainer can have a Suffix type that’s different from itself—meaning the suffix operation can return a different type. For example, here’s an extension to the nongeneric IntStack type that adds SuffixableContainer conformance, using Stack<Int> as its suffix type instead of IntStack:

extension IntStack: SuffixableContainer {
   func suffix(_ size: Int) -> Stack<Int> {
       var result = Stack<Int>()
       for index in (count-size)..<count {
           result.append(self[index])
       }
       return result
   }
   // Inferred that Suffix is Stack<Int>.
}

Generic Where Clauses

Type constraints, as described in Type Constraints, enable you to define requirements on the type parameters associated with a generic function, subscript, or type.

It can also be useful to define requirements for associated types. You do this by defining a generic where clause. A generic where clause enables you to require that an associated type must conform to a certain protocol, or that certain type parameters and associated types must be the same. A generic where clause starts with the where keyword, followed by constraints for associated types or equality relationships between types and associated types. You write a generic where clause right before the opening curly brace of a type or function’s body.

The example below defines a generic function called allItemsMatch, which checks to see if two Containerinstances contain the same items in the same order. The function returns a Boolean value of true if all items match and a value of false if they don’t.

The two containers to be checked don’t have to be the same type of container (although they can be), but they do have to hold the same type of items. This requirement is expressed through a combination of type constraints and a generic where clause:

// 

Type Constraints 

<C1: Container, C2: Container> 이부분

// 

defining a generic where clause

func allItemsMatch<C1: Container, C2: Container>
   (_ someContainer: C1, _ anotherContainer: C2) -> Bool
   where C1.Item == C2.Item, C1.Item: Equatable {
       
       // Check that both containers contain the same number of items.
       if someContainer.count != anotherContainer.count {
           return false
       }
       
       // Check each pair of items to see if they’re equivalent.
       for i in 0..<someContainer.count {
           if someContainer[i] != anotherContainer[i] {
               return false
           }
       }
       
       // All items match, so return true.
       return true
}

This function takes two arguments called someContainer and anotherContainer. The someContainer argument is of type C1, and the anotherContainer argument is of type C2. Both C1 and C2 are type parameters for two container types to be determined when the function is called.

The following requirements are placed on the function’s two type parameters:

  • C1 must conform to the Container protocol (written as C1: Container).
  • C2 must also conform to the Container protocol (written as C2: Container).
  • The Item for C1 must be the same as the Item for C2 (written as C1.Item == C2.Item).
  • The Item for C1 must conform to the Equatable protocol (written as C1.Item: Equatable).

The first and second requirements are defined in the function’s type parameter list, and the third and fourth requirements are defined in the function’s generic where clause.

These requirements mean:

  • someContainer is a container of type C1.
  • anotherContainer is a container of type C2.
  • someContainer and anotherContainer contain the same type of items.
  • The items in someContainer can be checked with the not equal operator (!=) to see if they’re different from each other.

The third and fourth requirements combine to mean that the items in anotherContainer can also be checked with the != operator, because they’re exactly the same type as the items in someContainer.

These requirements enable the allItemsMatch(_:_:) function to compare the two containers, even if they’re of a different container type.

The allItemsMatch(_:_:) function starts by checking that both containers contain the same number of items. If they contain a different number of items, there’s no way that they can match, and the function returns false.

After making this check, the function iterates over all of the items in someContainer with a forin loop and the half-open range operator (..<). For each item, the function checks whether the item from someContainer isn’t equal to the corresponding item in anotherContainer. If the two items aren’t equal, then the two containers don’t match, and the function returns false.

If the loop finishes without finding a mismatch, the two containers match, and the function returns true.

Here’s how the allItemsMatch(_:_:) function looks in action:

var stackOfStrings = Stack<String>()
stackOfStrings.push(“uno”)
stackOfStrings.push(“dos”)
stackOfStrings.push(“tres”)
var arrayOfStrings = [“uno”, “dos”, “tres”]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
   print(“All items match.”)
} else {
   print(“Not all items match.”)
}
// Prints “All items match.”

The example above creates a Stack instance to store String values, and pushes three strings onto the stack. The example also creates an Array instance initialized with an array literal containing the same three strings as the stack. Even though the stack and the array are of a different type, they both conform to the Container protocol, and both contain the same type of values. You can therefore call the allItemsMatch(_:_:)function with these two containers as its arguments. In the example above, the allItemsMatch(_:_:) function correctly reports that all of the items in the two containers match.

Extensions with a Generic Where Clause

You can also use a generic where clause as part of an extension. The example below extends the generic Stack structure from the previous examples to add an isTop(_:) method.

// Element 는 

Equatable protocol을 conform해야만 한다.

extension Stack where Element: Equatable {
   func isTop(_ item: Element) -> Bool {

       // 맨위 아이템이 있는지 확인
       guard let topItem = items.last else {
           return false
       }
       return topItem == item
   }
}

This new isTop(_:) method first checks that the stack isn’t empty, and then compares the given item against the stack’s topmost item. If you tried to do this without a generic where clause, you would have a problem: The implementation of isTop(_:) uses the == operator, but the definition of Stack doesn’t require its items to be equatable, so using the == operator results in a compile-time error. Using a generic where clause lets you add a new requirement to the extension, so that the extension adds the isTop(_:) method only when the items in the stack are equatable.

Here’s how the isTop(_:) method looks in action:

if stackOfStrings.isTop(“tres”) {
   print(“Top element is tres.”)
} else {
   print(“Top element is something else.”)
}
// Prints “Top element is tres.”

If you try to call the isTop(_:) method on a stack whose elements aren’t equatable, you’ll get a compile-time error.

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()

// 임의로 

Equatable 를 conform 하지않는 요소 만들기
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // Error

You can use a generic where clause with extensions to a protocol. The example below extends the Containerprotocol from the previous examples to add a startsWith(_:) method.

extension Container where Item: Equatable {
   func startsWith(_ item: Item) -> Bool {
       return count >= 1 && self[0] == item
   }
}

The startsWith(_:) method first makes sure that the container has at least one item, and then it checks whether the first item in the container matches the given item. This new startsWith(_:) method can be used with any type that conforms to the Container protocol, including the stacks and arrays used above, as long as the container’s items are equatable.

if [9, 9, 9].startsWith(42) {
   print(“Starts with 42.”)
} else {
   print(“Starts with something else.”)
}
// Prints “Starts with something else.”

The generic where clause in the example above requires Item to conform to a protocol, but you can also write a generic where clauses that require Item to be a specific type. For example:

extension Container where Item == Double {
   func average() -> Double {
       var sum = 0.0
       for index in 0..<count {
           sum += self[index]
       }
       return sum / Double(count)
   }
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// Prints “648.9”

This example adds an average() method to containers whose Item type is Double. It iterates over the items in the container to add them up, and divides by the container’s count to compute the average. It explicitly converts the count from Int to Double to be able to do floating-point division.

You can include multiple requirements in a generic where clause that is part of an extension, just like you can for a generic where clause that you write elsewhere. Separate each requirement in the list with a comma.

Associated Types with a Generic Where Clause

You can include a generic where clause on an associated type. For example, suppose you want to make a version of Container that includes an iterator, like what the Sequence protocol uses in the standard library. Here’s how you write that:

protocol Container {
   associatedtype Item
   mutating func append(_ item: Item)
   var count: Int { get }
   subscript(i: Int) -> Item { get }
   
   associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
   func makeIterator() -> Iterator
}

The generic where clause on Iterator requires that the iterator must traverse over elements of the same item type as the container’s items, regardless of the iterator’s type. The makeIterator() function provides access to a container’s iterator.

For a protocol that inherits from another protocol, you add a constraint to an inherited associated type by including the generic where clause in the protocol declaration. For example, the following code declares a ComparableContainer protocol that requires Item to conform to Comparable:

protocol ComparableContainer: Container where Item: Comparable { }

Generic Subscripts

Subscripts can be generic, and they can include generic where clauses. You write the placeholder type name inside angle brackets after subscript, and you write a generic where clause right before the opening curly brace of the subscript’s body. For example:

extension Container {
   subscript<Indices: Sequence>(indices: Indices) -> [Item]
       where Indices.Iterator.Element == Int {
           var result = [Item]()
           for index in indices {
               result.append(self[index])
           }
           return result
   }
}

This extension to the Container protocol adds a subscript that takes a sequence of indices and returns an array containing the items at each given index. This generic subscript is constrained as follows:

The generic parameter Indices in angle brackets has to be a type that conforms to the Sequenceprotocol from the standard library.

The subscript takes a single parameter, indices, which is an instance of that Indices type.

The generic where clause requires that the iterator for the sequence must traverse over elements of type Int. This ensures that the indices in the sequence are the same type as the indices used for a container.

Taken together, these constraints mean that the value passed for the indices parameter is a sequence of integers.