If you are a Parse user looking for an alternative Backend as a Service solution, Firebase might be the ideal choice for your iOS app.
This guide describes how to integrate specific services into your app. For basic Firebase setup instructions, see the iOS+ Setup guide.
Google Analytics
Google Analytics is a free app measurement solution that provides insight on app usage and user engagement. Analytics integrates across Firebase features and provides you with unlimited reporting for up to 500 distinct events that you can define using the Firebase SDK.
See the Google Analytics docs to learn more.
Suggested Migration Strategy
Using different analytics providers is a common scenario that easily applies to Google Analytics. Just add it to your app to benefit from events and user properties that Analytics automatically collects, like first open, app update, device model, age.
For custom events and user properties, you can employ a double write strategy using both Parse Analytics and Google Analytics to log events and properties, which allows you to gradually roll out the new solution.
Code Comparison
Parse Analytics
// Start collecting data
[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];
NSDictionary *dimensions = @{
// Define ranges to bucket data points into meaningful segments
@"priceRange": @"1000-1500",
// Did the user filter the query?
@"source": @"craigslist",
// Do searches happen more often on weekdays or weekends?
@"dayType": @"weekday"
};
// Send the dimensions to Parse along with the 'search' event
[PFAnalytics trackEvent:@"search" dimensions:dimensions];
Google Analytics
// Obtain the AppMeasurement instance and start collecting data
[FIRApp configure];
// Send the event with your params
[FIRAnalytics logEventWithName:@"search" parameters:@{
// Define ranges to bucket data points into meaningful segments
@"priceRange": @"1000-1500",
// Did the user filter the query?
@"source": @"craigslist",
// Do searches happen more often on weekdays or weekends?
@"dayType": @"weekday"
}];
Firebase Realtime Database
The Firebase Realtime Database is a NoSQL cloud-hosted database. Data is stored as JSON and synchronized in real time to every connected client.
See the Firebase Realtime Database docs to learn more.
Differences With Parse Data
Objects
In Parse you store a PFObject
, or a subclass of it, that contains key-value pairs
of JSON-compatible data. The data is schemaless, which means you don't need to specify what keys
exists on each PFObject
.
All Firebase Realtime Database data is stored as JSON objects, and there is no equivalent for
PFObject
; you simply write to the JSON tree values of types that correspond
to the available JSON types.
The following is an example of how you might save the high scores for a game.
Parse
PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;
[gameScore saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// The object has been saved.
} else {
// There was a problem, check error.description
}
}];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
NSString *key = [[ref child:@"scores"] childByAutoId].key;
NSDictionary *score = @{@"score": @1337,
@"playerName": @"Sean Plott",
@"cheatMode": @NO};
[key setValue:score withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
if (error) {
// The object has been saved.
} else {
// There was a problem, check error.description
}
}];
Relationships Between Data
A PFObject
can have a relationship with another PFObject
: any
object can use other objects as values.
In the Firebase Realtime Database, relations are better expressed using flat data structures that split the data into separate paths, so that they can be efficiently downloaded in separate calls.
The following is an example of how you might structure the relationship between posts in a blogging app and their authors.
Parse
// Create the author
PFObject *myAuthor = [PFObject objectWithClassName:@"Author"];
myAuthor[@"name"] = @"Grace Hopper";
myAuthor[@"birthDate"] = @"December 9, 1906";
myAuthor[@"nickname"] = @"Amazing Grace";
// Create the post
PFObject *myPost = [PFObject objectWithClassName:@"Post"];
myPost[@"title"] = @"Announcing COBOL, a New Programming Language";
// Add a relation between the Post and the Author
myPost[@"parent"] = myAuthor;
// This will save both myAuthor and myPost
[myPost saveInBackground];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
// Create the author
NSString *myAuthorKey = @"ghopper";
NSDictionary *author = @{@"name": @"Grace Hopper",
@"birthDate": @"December 9, 1906",
@"nickname": @"Amazing Grace"};
// Save the author
[[ref child:myAuthorKey] setValue:author]
// Create and save the post
NSString *key = [[ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"author": myAuthorKey,
@"title": @"Announcing COBOL, a New Programming Language"};
[key setValue:post]
The following data layout is the result.
{ // Info about the authors "authors": { "ghopper": { "name": "Grace Hopper", "date_of_birth": "December 9, 1906", "nickname": "Amazing Grace" }, ... }, // Info about the posts: the "author" fields contains the key for the author "posts": { "-JRHTHaIs-jNPLXOQivY": { "author": "ghopper", "title": "Announcing COBOL, a New Programming Language" } ... } }
Reading Data
In Parse you read data using either the ID of a specific Parse object, or
executing queries using PFQuery
.
In Firebase, you retrieve data by attaching an asynchronous listener to a database reference. The listener is triggered once for the initial state of the data and again when the data changes, so you won't need to add any code to determine if the data changed.
The following is an example of how you can retrieve scores for a particular player, based on the example presented in the "Objects" section.
Parse
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerName" equalTo:@"Dan Stemkoski"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *score in objects) {
NSString *gameScore = score[@"score"];
NSLog(@"Retrieved: %@", gameScore);
}
} else {
// Log details of the failure
NSLog(@"Error: %@ %@", error, [error userInfo]);
}
}];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
// This type of listener is not one time, and you need to cancel it to stop
// receiving updates.
[[[[ref child:@"scores"] queryOrderedByChild:@"playerName"] queryEqualToValue:@"Dan Stemkoski"]
observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
// This will fire for each matching child node.
NSDictionary *score = snapshot.value;
NSString gameScore = score[@"score"];
NSLog(@"Retrieved: %@", gameScore);
}];
Suggested Migration Strategy
Rethink Your Data
The Firebase Realtime Database is optimized to sync data in milliseconds across all connected clients, and the resulting data structure is different from the Parse core data. This means that the first step of your migration is to consider what changes your data requires, including:
- How your Parse objects should map to Firebase data
- If you have parent-child relations, how to split your data across different paths so that it can be efficiently downloaded in separate calls.
Migrate Your Data
After you decide how to structure your data in Firebase, you need to plan how to handle the period during which your app needs to write to both databases. Your choices are:
Background Sync
In this scenario, you have two versions of the app: the old version that uses Parse and a new version that uses Firebase. Syncs between the two databases are handled by Parse Cloud Code (Parse to Firebase), with your code listening to changes on Firebase and syncing those changes with Parse. Before you can start using the new version, you must:
- Convert your existing Parse Data to the new Firebase structure, and write it to the Firebase Realtime Database.
- Write Parse Cloud Code functions that use the Firebase REST API to write to the Firebase Realtime Database changes made in the Parse Data by old clients.
- Write and deploy code that listens to changes on Firebase and syncs them to the Parse database.
This scenario ensures a clean separation of old and new code, and keeps the clients simple. The challenges of this scenario are handling big datasets in the initial export, and ensuring that the bidirectional sync doesn't generate infinite recursion.
Double Write
In this scenario, you write a new version of the app that uses both Firebase and Parse, using Parse Cloud Code to sync changes made by old clients from the Parse Data to the Firebase Realtime Database. When enough people have migrated from the Parse-only version of the app, you can remove the Parse code from the double write version.
This scenario doesn't require any server side code. Its disadvantages are that data that is not accessed is not migrated, and that the size of your app is increased by the usage of both SDKs.
Firebase Authentication
Firebase Authentication can authenticate users using passwords and popular federated identity providers like Google, Facebook and Twitter. It also provides UI libraries to save you the significant investment required to implement and maintain a full authentication experience for your app across all platforms.
See the Firebase Authentication docs to learn more.
Differences With Parse Auth
Parse provides a specialized user class called PFUser
that automatically handles
the functionality required for user account management. PFUser
is a subclass of the
PFObject
, which means user data is available in the Parse Data and can be extended with
additional fields like any other PFObject
.
A FIRUser
has a fixed set of basic properties—a unique ID, a primary email address,
a name and a photo URL—stored in a separate project's user database; those properties can be updated by
the user. You cannot add other properties to the FIRUser
object directly;
instead, you can store the additional properties in your Firebase Realtime Database.
The following is an example of how you might sign up a user and add an additional phone number field.
Parse
PFUser *user = [PFUser user];
user.username = @"my name";
user.password = @"my pass";
user.email = @"email@example.com";
// other fields can be set just like with PFObject
user[@"phone"] = @"415-392-0202";
[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (!error) {
// Hooray! Let them use the app now.
} else {
// Something went wrong
NSString *errorString = [error userInfo][@"error"];
}
}];
Firebase
[[FIRAuth auth] createUserWithEmail:@"email@example.com"
password:@"my pass"
completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
if (!error) {
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[[[ref child:@"users"] child:user.uid] child:@"phone"] setValue:@"415-392-0202"
} else {
// Something went wrong
NSString *errorString = [error userInfo][@"error"];
}
}];
Suggested Migration Strategy
Migrate Accounts
To migrate user accounts from Parse to Firebase, export your user database to
a JSON or CSV file, then import the file into your Firebase project using the
Firebase CLI's auth:import
command.
First, export your user database from the Parse console or your self-hosted database. For example, a JSON file exported from the Parse console might look like the following:
{ // Username/password user "bcryptPassword": "$2a$10$OBp2hxB7TaYZgKyTiY48luawlTuYAU6BqzxJfpHoJMdZmjaF4HFh6", "email": "user@example.com", "username": "testuser", "objectId": "abcde1234", ... }, { // Facebook user "authData": { "facebook": { "access_token": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "expiration_date": "2017-01-02T03:04:05.006Z", "id": "1000000000" } }, "username": "wXyZ987654321StUv", "objectId": "fghij5678", ... }
Then, transform the exported file into the format required by the Firebase
CLI. Use the objectId
of your Parse users as the
localId
of your Firebase users. Also, base64 encode the
bcryptPassword
values from Parse and use them in the passwordHash
field. For example:
{ "users": [ { "localId": "abcde1234", // Parse objectId "email": "user@example.com", "displayName": "testuser", "passwordHash": "JDJhJDEwJE9CcDJoeEI3VGFZWmdLeVRpWTQ4bHVhd2xUdVlBVTZCcXp4SmZwSG9KTWRabWphRjRIRmg2", }, { "localId": "fghij5678", // Parse objectId "displayName": "wXyZ987654321StUv", "providerUserInfo": [ { "providerId": "facebook.com", "rawId": "1000000000", // Facebook ID } ] } ] }
Finally, import the transformed file with the Firebase CLI, specifying bcrypt as the hash algorithm:
firebase auth:import account_file.json --hash-algo=BCRYPT
Migrate User Data
If you are storing additional data for your users, you can migrate it to Firebase Realtime Database using the strategies described in the data migration section. If you migrate accounts using the flow described in the accounts migration section, your Firebase accounts have the same ids of your Parse accounts, allowing you to easily migrate and reproduce any relations keyed by the user id.
Firebase Cloud Messaging
Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost. The Notifications composer is a no-cost service built on Firebase Cloud Messaging that enables targeted user notifications for mobile app developers.
See the Firebase Cloud Messaging docs to learn more.
Differences With Parse Push Notifications
Every Parse application installed on a device registered for notifications has an associated
Installation
object, where you store all the data needed to target notifications.
Installation
is a subclass of PFUser
, which means you can add
any additional data you want to your Installation
instances.
The Notifications composer provides predefined user segments based on information like app, app version and device language. You can build more complex user segments using Google Analytics events and properties to build audiences. See the audiences help guide to learn more. These targeting informations are not visible in the Firebase Realtime Database.
Suggested Migration Strategy
Migrating Device Tokens
While Parse uses APNs device tokens to target installations for notifications, FCM uses FCM registration tokens mapped to the APNs device tokens. Just add the FCM SDK to your Apple app and it will fetch an FCM token automatically.
Migrating Channels To FCM Topics
If you are using Parse channels to send notifications, you can migrate to FCM topics, which provide the same publisher-subscriber model. To handle the transition from Parse to FCM, you can write a new version of the app that uses the Parse SDK to unsubscribe from Parse channels and the FCM SDK to subscribe to corresponding FCM topics.
For example, if your user is subscribed to the "Giants" topic, you would do something like:
PFInstallation *currentInstallation = [PFInstallation currentInstallation];
[currentInstallation removeObject:@"Giants" forKey:@"channels"];
[currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
[[FIRMessaging messaging] subscribeToTopic:@"/topics/Giants"];
} else {
// Something went wrong unsubscribing
}
}];
Using this strategy, you can send messages to both the Parse channel and the corresponding FCM topic, supporting users of both old and new versions. When enough users have migrated from the Parse-only version of the app, you can sunset that version and start sending using FCM only.
See the FCM topics docs to learn more.
Firebase Remote Config
Firebase Remote Config is a cloud service that lets you change the behavior and appearance of your app without requiring users to download an app update. When using Remote Config, you create in-app default values that control the behavior and appearance of your app. Then, you can later use the Firebase console to override in-app default values for all app users or for segments of your userbase.
Firebase Remote Config can be very useful during your migrations in cases where you want to test different solutions and be able to dynamically shift more clients to a different provider. For example, if you have a version of your app that uses both Firebase and Parse for the data, you could use a random percentile rule to determine which clients read from Firebase, and gradually increase the percentage.
To learn more about Firebase Remote Config, see the Remote Config introduction.
Differences With Parse Config
With Parse config you can add key/value pairs to your app on the Parse Config Dashboard, and then
fetch the PFConfig
on the client. Every PFConfig
instance that you
get is always immutable. When you retrieve a new PFConfig
in the future from the
network, it will not modify any existing PFConfig
instance, but will instead
create a new one and make it available via currentConfig
.
With Firebase Remote Config you create in-app defaults for key/value pairs that you can override from the Firebase console, and you can use rules and conditions to provide variations on your app's user experience to different segments of your userbase. Firebase Remote Config implements a singleton class that makes the key/value pairs available to your app. Initially the singleton returns the default values that you define in-app. You can fetch a new set of values from the server at any moment convenient for your app; after the new set is successfully fetched, you can choose when to activate it to make the new values available to the app.
Suggested Migration Strategy
You can move to Firebase Remote Config by copying the key/value pairs of your Parse config to the Firebase console, and then deploying a new version of the app that uses Firebase Remote Config.
If you want to experiment with both Parse Config and Firebase Remote Config, you can deploy a new version of the app that uses both SDKs until enough users have migrated from the Parse only version.
Code Comparison
Parse
[PFConfig getConfigInBackgroundWithBlock:^(PFConfig *config, NSError *error) {
if (!error) {
NSLog(@"Yay! Config was fetched from the server.");
} else {
NSLog(@"Failed to fetch. Using Cached Config.");
config = [PFConfig currentConfig];
}
NSString *welcomeMessage = config[@"welcomeMessage"];
if (!welcomeMessage) {
NSLog(@"Falling back to default message.");
welcomeMessage = @"Welcome!";
}
}];
Firebase
FIRRemoteConfig remoteConfig = [FIRRemoteConfig remoteConfig];
// Set defaults from a plist file
[remoteConfig setDefaultsFromPlistFileName:@"RemoteConfigDefaults"];
[remoteConfig fetchWithCompletionHandler:^(FIRRemoteConfigFetchStatus status, NSError *error) {
if (status == FIRRemoteConfigFetchStatusSuccess) {
NSLog(@"Yay! Config was fetched from the server.");
// Once the config is successfully fetched it must be activated before newly fetched
// values are returned.
[self.remoteConfig activateFetched];
} else {
NSLog(@"Failed to fetch. Using last fetched or default.");
}
}];
// ...
// When this is called, the value of the latest fetched and activated config is returned;
// if there's none, the default value is returned.
NSString welcomeMessage = remoteConfig[@"welcomeMessage"].stringValue;