Get a FIRDatabaseReference
To read or write data from the database, you need an instance of
FIRDatabaseReference
:
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Swift
var ref: FIRDatabaseReference! ref = FIRDatabase.database().reference()
Reading and writing data
This document covers the basics of reading and writing Firebase data.
Firebase data is written to a FIRDatabase
reference and retrieved by
attaching an asynchronous listener to the reference. The listener is triggered
once for the initial state of the data and again anytime the data changes.
Basic write operations
For basic write operations, you can use setValue
to save data to a specified
reference, replacing any existing data at that path. You can use this method to:
- Pass types that correspond to the available JSON types as follows:
NSString
NSNumber
NSDictionary
NSArray
For instance, you can add a user with setValue
as follows:
Objective-C
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username}];
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Using setValue
in this way overwrites data at the specified location,
including any child nodes. However, you can still update a child without
rewriting the entire object. If you want to allow users to update their profiles
you could update the username as follows:
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Swift
self.ref.child("users/(user.uid)/username").setValue(username)
Listen for value events
To read data at a path and listen for changes, use the
observeEventType:withBlock
orobserveSingleEventOfType:withBlock
methods of FIRDatabaseReference
to observe FIRDataEventTypeValue
events.
Event type | Typical usage |
---|---|
FIRDataEventTypeValue |
Read and listen for changes to the entire contents of a path. |
You can use the FIRDataEventTypeValue
event to read the data at a given path,
as it exists at the time of the event. This method is triggered once when the
listener is attached and again every time the data, including any children,
changes. The event callback is passed a snapshot
containing all data at that
location, including child data. If there is no data, the value
of the
snapshot
returned is nil
.
The following example demonstrates a social blogging application retrieving the details of a post from the database:
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
Swift
refHandle = postRef.observe(FIRDataEventType.value, with: { (snapshot) in let postDict = snapshot.value as? [String : AnyObject] ?? [:] // ... })
The listener receives a FIRDataSnapshot
that contains the data at the specified
location in the database at the time of the event in its value
property. You
can assign the values to the appropriate native type, such as NSDictionary
.
If no data exists at the location, the value
is nil
.
Read data once
In some cases you may want a callback to be called once and then immediately
removed, such as when initializing a UI element that you don't expect to change.
You can use the observeSingleEventOfType
method to simplify this scenario:
the event callback added triggers once and then does not trigger again.
This is useful for data that only needs to be loaded once and isn't expected to change frequently or require active listening. For instance, the blogging app in the previous examples uses this method to load a user's profile when they begin authoring a new post:
Objective-C
NSString *userID = [FIRAuth auth].currentUser.uid; [[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { // Get user value User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]]; // ... } withCancelBlock:^(NSError * _Nonnull error) { NSLog(@"%@", error.localizedDescription); }];
Swift
let userID = FIRAuth.auth()?.currentUser?.uid ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in // Get user value let value = snapshot.value as? NSDictionary let username = value?["username"] as? String ?? "" let user = User.init(username: username) // ... }) { (error) in print(error.localizedDescription) }
Updating or deleting data
Update specific fields
To simultaneously write to specific children of a node without overwriting other
child nodes, use the updateChildValues
method.
When calling updateChildValues
, you can update lower-level child values by
specifying a path for the key. If data is stored in multiple locations to scale
better, you can update all instances of that data using
data fan-out. For example, a
social blogging app might want to create a post and simultaneously update it to
the recent activity feed and the posting user's activity feed. To do this, the
blogging application uses code like this:
Objective-C
NSString *key = [[_ref child:@"posts"] childByAutoId].key; NSDictionary *post = @{@"uid": userID, @"author": username, @"title": title, @"body": body}; NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post, [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post}; [_ref updateChildValues:childUpdates];
Swift
let key = ref.child("posts").childByAutoId().key let post = ["uid": userID, "author": username, "title": title, "body": body] let childUpdates = ["/posts/\(key)": post, "/user-posts/\(userID)/\(key)/": post] ref.updateChildValues(childUpdates)
This example uses childByAutoId
to create a post in the node containing posts for
all users at /posts/$postid
and simultaneously retrieve the key with
getKey()
. The key can then be used to create a second entry in the user's
posts at /user-posts/$userid/$postid
.
Using these paths, you can perform simultaneous updates to multiple locations in
the JSON tree with a single call to updateChildValues
, such as how this example
creates the new post in both locations. Simultaneous updates made this way
are atomic: either all updates succeed or all updates fail.
Delete data
The simplest way to delete data is to call removeValue
on a reference to the
location of that data.
You can also delete by specifying nil
as the value for another write
operation such as setValue
or updateChildValues
. You can use this technique
with updateChildValues
to delete multiple children in a single API call.
Detach listeners
Observers don't automatically stop syncing data when you leave a
ViewController
. If an observer isn't properly removed, it continues to sync
data to local memory. When an observer is no longer needed, remove it by passing
the associated FIRDatabaseHandle
to the removeObserverWithHandle
method.
When you add a callback block to a reference, a FIRDatabaseHandle
is returned.
These handles can be used to remove the callback block.
If multiple listeners have been added to a database reference, each listener is
called when an event is raised. In order to stop syncing data at that location,
you must remove all observers at a location by calling the removeAllObservers
method.
Calling removeObserverWithHandle
or removeAllObservers
on a listener does
not automatically remove listeners registered on its child nodes; you must also
be keep track of those references or handles to remove them.
Save data as transactions
When working with data that could be corrupted by concurrent modifications, such as incremental counters, you can use a transaction operation. You give this operation two arguments: an update function and an optional completion callback. The update function takes the current state of the data as an argument and returns the new desired state you would like to write.
For instance, in the example social blogging app, you could allow users to star and unstar posts and keep track of how many stars a post has received as follows:
Objective-C
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { NSMutableDictionary *post = currentData.value; if (!post || [post isEqual:[NSNull null]]) { return [FIRTransactionResult successWithValue:currentData]; } NSMutableDictionary *stars = [post objectForKey:@"stars"]; if (!stars) { stars = [[NSMutableDictionary alloc] initWithCapacity:1]; } NSString *uid = [FIRAuth auth].currentUser.uid; int starCount = [post[@"starCount"] intValue]; if ([stars objectForKey:uid]) { // Unstar the post and remove self from stars starCount--; [stars removeObjectForKey:uid]; } else { // Star the post and add self to stars starCount++; stars[uid] = @YES; } post[@"stars"] = stars; post[@"starCount"] = [NSNumber numberWithInt:starCount]; // Set value and report transaction success [currentData setValue:post]; return [FIRTransactionResult successWithValue:currentData]; } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { // Transaction completed if (error) { NSLog(@"%@", error.localizedDescription); } }];
Swift
ref.runTransactionBlock({ (currentData: FIRMutableData) -> FIRTransactionResult in if var post = currentData.value as? [String : AnyObject], let uid = FIRAuth.auth()?.currentUser?.uid { var stars: Dictionary<String, Bool> stars = post["stars"] as? [String : Bool] ?? [:] var starCount = post["starCount"] as? Int ?? 0 if let _ = stars[uid] { // Unstar the post and remove self from stars starCount -= 1 stars.removeValue(forKey: uid) } else { // Star the post and add self to stars starCount += 1 stars[uid] = true } post["starCount"] = starCount as AnyObject? post["stars"] = stars as AnyObject? // Set value and report transaction success currentData.value = post return FIRTransactionResult.success(withValue: currentData) } return FIRTransactionResult.success(withValue: currentData) }) { (error, committed, snapshot) in if let error = error { print(error.localizedDescription) } }
Using a transaction prevents star counts from being incorrect if multiple
users star the same post at the same time or the client had stale data. The
value contained in the FIRMutableData
class is initially the client's last
known value for the path, or nil
if there is none. The server compares the
initial value against it's current value and accepts the transaction if the
values match, or rejects it. If the transaction is rejected, the server returns
the current value to the client, which runs the transaction again with the
updated value. This repeats until the transaction is accepted or too many
attempts have been made.
Write data offline
If a client loses its network connection, your app will continue functioning correctly.
Every client connected to a Firebase database maintains its own internal version of any active data. When data is written, it's written to this local version first. The Firebase client then synchronizes that data with the remote database servers and with other clients on a "best-effort" basis.
As a result, all writes to the database trigger local events immediately, before any data is written to the server. This means your app remains responsive regardless of network latency or connectivity.
Once connectivity is reestablished, your app receives the appropriate set of events so that the client syncs with the current server state, without having to write any custom code.