The easiest way to integrate the Parse SDK into your iOS, iPadOS, macOS, watchOS, tvOS app is via Swift Package Manager (SPM).
https://github.com/parse-community/Parse-SDK-iOS-OSX
Note: You may have to add submodules under Link Binary with Libraries
To initialize the Parse client, add the following to your AppDelegate.swift file (AppDelegate.m for Objective-C), in the application:didFinishLaunchingWithOptions:
method.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[Parse initializeWithConfiguration:[ParseClientConfiguration configurationWithBlock:^(id<ParseMutableClientConfiguration> configuration) {
configuration.applicationId = @"parseAppId";
configuration.clientKey = @"parseClientKey";
configuration.server = @"parseServerUrlString";
}]];
return YES;
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let parseConfig = ParseClientConfiguration {
$0.applicationId = "parseAppId"
$0.clientKey = "parseClientKey"
$0.server = "parseServerUrlString"
}
Parse.initialize(with: parseConfig)
return true
}
Make sure to import the Parse module at the top of any file in which you want to use the Parse SDK by including the follwing.
@import ParseCore;
import ParseCore
Storing data on Parse is built around the PFObject
. Each PFObject
contains key-value pairs of JSON-compatible data. This data is schemaless, which means that you don’t need to specify ahead of time what keys exist on each PFObject
. You simply set whatever key-value pairs you want, and our backend will store it.
For example, let’s say you’re tracking high scores for a game. A single PFObject
could contain:
score: 1337, playerName: "Sean Plott", cheatMode: false
Keys must be alphanumeric strings. Values can be strings, numbers, booleans, or even arrays and dictionaries - anything that can be JSON-encoded.
Each PFObject
has a class name that you can use to distinguish different sorts of data. For example, we could call the high score object a GameScore
. We recommend that you NameYourClassesLikeThis and nameYourKeysLikeThis, just to keep your code looking pretty.
Let’s say you want to save the GameScore
described above to a Parse Server. The interface is similar to a NSMutableDictionary
. The saveInBackgroundWithBlock
function:
PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;
[gameScore saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// The object has been saved.
} else {
// There was a problem, check error.description
}
}];
let gameScore = PFObject(className:"GameScore")
gameScore["score"] = 1337
gameScore["playerName"] = "Sean Plott"
gameScore["cheatMode"] = false
gameScore.saveInBackground { (succeeded, error) in
if (succeeded) {
// The object has been saved.
} else {
// There was a problem, check error.description
}
}
After this code runs, you will probably be wondering if anything really happened. If Parse Dashboard is implemented for your server, you can verify the data was saved in the data browser. You should see something like this:
{
"objectId": "xWMyZ4YEGZ",
"score": 1337,
"playerName": "Sean Plott",
"cheatMode": false,
"createdAt":"2022-01-01T12:23:45.678Z",
"updatedAt":"2022-01-01T12:23:45.678Z"
}
There are two things to note here. You didn’t have to configure or set up a new Class called GameScore
before running this code. Your Parse app lazily creates this Class for you when it first encounters it.
There are also a few fields you don’t need to specify that are provided and set by the system as a convenience. objectId
is a unique identifier for each saved object. createdAt
and updatedAt
represent the time that each object was created or last modified and saved to the Parse Server. Each of these fields is filled in by Parse Server, so they don’t exist on a PFObject
until the first save operation has been completed.
Saving data to the cloud is fun, but it’s even more fun to get that data out again. If the PFObject
has been uploaded to the server, you can retrieve it with its objectId
by using a PFQuery
. This is an asynchronous method, with variations for using either blocks or callback methods:
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query getObjectInBackgroundWithId:@"xWMyZ4YEGZ" block:^(PFObject *gameScore, NSError *error) {
if (!error) {
// Success!
} else {
// Failure!
}
}];
let query = PFQuery(className:"GameScore")
query.getObjectInBackground(withId: "xWMyZEGZ") { (gameScore, error) in
if error == nil {
// Success!
} else {
// Fail!
}
}
To get the values out of the PFObject
, you can use either the objectForKey:
method or the []
subscripting operator:
int score = [[gameScore objectForKey:@"score"] intValue];
NSString *playerName = gameScore[@"playerName"];
BOOL cheatMode = [gameScore[@"cheatMode"] boolValue];
let score = gameScore["score"] as? Int
let playerName = gameScore["playerName"] as? String
let cheatMode = gameScore["cheatMode"] as? Bool
The four special values are provided as properties:
NSString *objectId = gameScore.objectId;
NSDate *updatedAt = gameScore.updatedAt;
NSDate *createdAt = gameScore.createdAt;
PFACL *ACL = gameScore.ACL;
let objectId = gameScore.objectId
let updatedAt = gameScore.updatedAt
let createdAt = gameScore.createdAt
let acl = gameScore.acl
If you need to refresh an object you already have with the latest data that
is in the database, you can use the fetchInBackgroundWithBlock:
or fetchInBackgroundWithTarget:selector:
methods.
[myObject fetchInBackgroundWithBlock:^(PFObject * _Nullable object, NSError * _Nullable error) {
if (!error) {
// Success!
} else {
// Failure!
}
}];
myObject.fetchInBackground { (object, error) in
if error == nil {
// Success!
} else {
// Failure!
}
}
Note: In a similar way to the save
methods, you can use the throwable fetch
or fetchIfNeeded
methods, or asyncronous task without completion. fetchInBackground
Parse also lets you store objects in a local datastore on the device itself. You can use this for data that doesn’t need to be saved to the cloud, but this is especially useful for temporarily storing data so that it can be synced later. To enable the datastore, add isLocalDatastoreEnabled = true
to the ParseClientConfiguration
block in your AppDelegate
application:didFinishLaunchWithOptions:
, or call Parse.enableLocalDatastore()
before calling Parse.initialize()
. Once the local datastore is enabled, you can store an object by pinning it.
PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = 1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;
[gameScore pinInBackground];
let gameScore = PFObject(className:"GameScore")
gameScore["score"] = 1337
gameScore["playerName"] = "Sean Plott"
gameScore["cheatMode"] = false
gameScore.pinInBackground()
As with saving, this recursively stores every object and file that gameScore
points to, if it has been fetched from the cloud. Whenever you save changes to the object, or fetch new changes from Parse, the copy in the datastore will be automatically updated, so you don’t have to worry about it.
Storing an object is only useful if you can get it back out. To get the data for a specific object, you can use a PFQuery
just like you would while on the network, but using the fromLocalDatastore:
method to tell it where to get the data.
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query fromLocalDatastore];
[[query getObjectInBackgroundWithId:@"xWMyZ4YEGZ"] continueWithBlock:^id(BFTask *task) {
if (task.error) {
// something went wrong;
return task;
}
// task.result will be your game score
return task;
}];
let query = PFQuery(className:"GameScore")
query.fromLocalDatastore()
query.getObjectInBackground(withId: "xWMyZEGZ").continueWith { (task: BFTask<PFObject>!) -> Any? in
if task.error != nil {
// There was an error.
return task
}
// task.result will be your game score
return task
}
If you already have an instance of the object, you can instead use the fetchFromLocalDatastoreInBackground:
method.
PFObject *object = [PFObject objectWithoutDataWithClassName:@"GameScore" objectId:@"xWMyZ4YEGZ"];
[[object fetchFromLocalDatastoreInBackground] continueWithBlock:^id(BFTask *task) {
if (task.error) {
// something went wrong
return task;
}
// task.result will be your game score
return task;
}];
let object = PFObject(withoutDataWithClassName:"GameScore", objectId:"xWMyZ4YEGZ")
object.fetchFromLocalDatastoreInBackground().continueWith { (task: BFTask<PFObject>!) -> Any? in
if task.error != nil {
// There was an error.
return task
}
// task.result will be your game score
return task
}
When you are done with the object and no longer need to keep it on the device, you can release it with unpinInBackground:
.
[gameScore unpinInBackground];
gameScore.unpinInBackground()
Most save functions execute immediately, and inform your app when the save is complete. For a network consious soltion on non-priority save requests use saveEventually
. Not only does it retry saving upon regaining network connection, but If your app is closed prior to save completion Parse will try the next time the app is opened. Additionally, all calls to saveEventually
(and deleteEventually
) are executed in the order they are called, making it safe to call saveEventually
on an object multiple times.
// Create the object.
PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;
[gameScore saveEventually];
let gameScore = PFObject(className:"GameScore")
gameScore["score"] = 1337
gameScore["playerName"] = "Sean Plott"
gameScore["cheatMode"] = false
gameScore.saveEventually()
Updating an object is simple. Just set some new data on it and call one of the save methods. Assuming you have saved the object and have the objectId
, you can retrieve the PFObject
using a PFQuery
and update its data:
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
// Retrieve the object by id
[query getObjectInBackgroundWithId:@"xWMyZ4YEGZ"
block:^(PFObject *gameScore, NSError *error) {
// Now let's update it with some new data. In this case, only cheatMode and score
// will get sent to the cloud. playerName hasn't changed.
gameScore[@"cheatMode"] = @YES;
gameScore[@"score"] = @1338;
[gameScore saveInBackground];
}];
let query = PFQuery(className:"GameScore")
query.getObjectInBackground(withId: "xWMyZEGZ") { (gameScore: PFObject?, error: Error?) in
if let error = error {
print(error.localizedDescription)
} else if let gameScore = gameScore {
gameScore["cheatMode"] = true
gameScore["score"] = 1338
gameScore.saveInBackground()
}
}
The client automatically figures out which data has changed so only “dirty” fields will be sent to Parse. You don’t need to worry about squashing data that you didn’t intend to update.
The above example contains a common use case. The “score” field is a counter that we’ll need to continually update with the player’s latest score. Using the above method works but it’s cumbersome and can lead to problems if you have multiple clients trying to update the same counter.
To help with storing counter-type data, Parse provides methods that atomically increment (or decrement) any number field. So, the same update can be rewritten as:
[gameScore incrementKey:@"score"];
[gameScore saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// The score key has been incremented
} else {
// There was a problem, check error.description
}
}];
let gameScore = PFObject(className:"GameScore")
gameScore.incrementKey("score")
gameScore.saveInBackground {
(success: Bool, error: Error?) in
if (success) {
// The score key has been incremented
} else {
// There was a problem, check error.description
}
}
You can also increment by any amount using incrementKey:byAmount:
.
To help with storing array data, there are three operations that can be used to atomically change an array field:
addObject:forKey:
and addObjectsFromArray:forKey:
append the given objects to the end of an array field.addUniqueObject:forKey:
and addUniqueObjectsFromArray:forKey:
add only the given objects which aren’t already contained in an array field to that field. The position of the insert is not guaranteed.removeObject:forKey:
and removeObjectsInArray:forKey:
remove all instances of each given object from an array field.For example, we can add items to the set-like “skills” field like so:
[gameScore addUniqueObjectsFromArray:@[@"flying", @"kungfu"] forKey:@"skills"];
[gameScore saveInBackground];
gameScore.addUniqueObjects(from: ["flying", "kungfu"], forKey:"skills")
gameScore.saveInBackground()
Note that it is not currently possible to atomically add and remove items from an array in the same save using. You will have to call save
in between every different kind of array operation.
There are a few ways to delete a PFObject
. For basic asynchronous deletion of a single object call the objects deleteInBackground
function. If you prefer to recieve a callback you can use the deleteInBackgroundWithBlock:
or deleteInBackgroundWithTarget:selector:
methods. If you want to block the calling thread, you can use the delete
method. Lastly, deleteEventually
is a network conscious option that deletes when possible but does not guarantee a timeframe for the tasks completion.
For deleting multiple objects use the PFObject
static function deleteAllInBackground
to delete an array of objects asynchronously. The same can be done while blocking the calling thread using deleteAll
. Lastly, to recieve a callback after deleting objects asyncronously use deleteAllInBackground:block:
as demonstrated below.
[PFObject deleteAllInBackground:objectArray block:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// The array of objects was successfully deleted.
} else {
// There was an error. Check the errors localizedDescription.
}
}];
PFObject.deleteAll(inBackground: objectArray) { (succeeded, error) in
if (succeeded) {
// The array of objects was successfully deleted.
} else {
// There was an error. Check the errors localizedDescription.
}
}
Note: Deleting an object from the server that contains a PFFileObject
does NOT delete the file from storage. Instead, an objects deletion only deletes the data referencing the stored file. To delete the data from storage you must use the REST API. For more info about PFFileObject
, please see the Files section.
Objects can have relationships with other objects. To model this behavior, any PFObject
can be used as a value in other PFObject
s. Internally, the Parse framework will store the referred-to object in just one place, to maintain consistency.
For example, each Comment
in a blogging app might correspond to one Post
. To create a new Post
with a single Comment
, you could write:
// Create the post
PFObject *myPost = [PFObject objectWithClassName:@"Post"];
myPost[@"title"] = @"I'm Hungry";
myPost[@"content"] = @"Where should we go for lunch?";
// Create the comment
PFObject *myComment = [PFObject objectWithClassName:@"Comment"];
myComment[@"content"] = @"Let's do Sushirrito.";
// Add a relation between the Post and Comment
myComment[@"post"] = myPost;
// This will save both myPost and myComment
[myComment saveInBackground];
// Create the post
let myPost = PFObject(className:"Post")
myPost["title"] = "I'm Hungry"
myPost["content"] = "Where should we go for lunch?"
// Create the comment
let myComment = PFObject(className:"Comment")
myComment["content"] = "Let's do Sushirrito."
// Add a relation between the Post and Comment
myComment["post"] = myPost
// This will save both myPost and myComment
myComment.saveInBackground()
Note: Saving an object with a relational pointer to another object will save both objects. However, two new objects with pointers to each other will cause a error for having a circular dependency.
You can link objects without even fetching data by initializing PFObjects
with only the class name and the objects objectId
like so:
myComment[@"post"] = [PFObject objectWithoutDataWithClassName:@"Post" objectId:@"1zEcyElZ80"];
// Add a relation between the Post with objectId "1zEcyElZ80" and the comment
myComment["post"] = PFObject(withoutDataWithClassName: "Post", objectId: "1zEcyElZ80")
By default, when fetching an object, related PFObject
s are not fetched. These objects’ values cannot be retrieved until they have been fetched like so:
PFObject *post = myComment[@"post"];
[post fetchInBackgroundWithBlock:^(PFObject * _Nullable object, NSError * _Nullable error) {
NSString *title = post[@"title"];
if (title) { // do something with title }
}];
let post = myComment["post"] as! PFObject
post.fetchIfNeededInBackground { (object, error) in
if let title = post["title"] as? String {
// do something with your title variable
} else if let errorString = error?.localizedDescription {
print(errorString)
}
}
You can also model a many-to-many relation using the PFRelation
object. This works similar to an NSArray
of PFObjects
, except that you don’t need to download all the Objects in a relation at once. This allows PFRelation
to scale to many more objects than the NSArray
of PFObject
approach. For example, a User
may have many Post
s that they might like. In this case, you can store the set of Post
s that a User
likes using relationForKey:
. In order to add a post to the list, the code would look something like:
PFUser *user = [PFUser currentUser];
PFRelation *relation = [user relationForKey:@"likes"];
[relation addObject:post];
[user saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// The post has been added to the user's likes relation.
} else {
// There was a problem, check error.description
}
}];
guard let user = PFUser.current() else { return }
let relation = user.relation(forKey: "likes")
relation.add(post)
user.saveInBackground { (succeeded, error) in
if (succeeded) {
// The post has been added to the user's likes relation.
} else {
// There was a problem, check error.description
}
}
You can remove a post from the PFRelation
similarly using the removeObject:
function followed by saving the parent object.
By default, the list of objects in this relation are not downloaded. You can get the list of Post
s by using calling findObjectsInBackgroundWithBlock:
on the PFQuery
returned by query
. The code would look like:
[[relation query] findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if (error) {
// There was an error
} else {
// objects has all the Posts the current user liked.
}
}];
relation.query().findObjectsInBackground { (object, error) in
if error == nil {
// Success
} else {
// Failure!
}
})
You can add constraints to a PFRelation
’s query by adding constraints to the PFQuery
returned by its query
parameter as demonstrated below:
PFQuery *query = [relation query];
[query whereKey:"category" equalTo:@"development"];
PFObject *object = [query getFirstObject]; // Query first object found
if (object) {
// Do something with object
}
var query = relation.query()
query.whereKey("category", equalTo: "development")
// Query first object found
if let object = try? query.getFirstObject() {
// Do something with object
} ```
</div>
You can learn more about queries by visiting the [PFQuery](#queries) section. A `PFRelation` behaves similarly to an array of `PFObject` yet has a built in query that's capable of everything a standard `PFQuery` is other than `includeKey:`.
## Data Types
So far we've used values with type `NSString`, `NSNumber`, and `PFObject`. Parse also supports `NSDate`, `NSObject`, `NSArray`, and `NSNull`. You can nest `NSObject` and `NSArray` objects to store more structured data within a single `PFObject`. Overall, the following types are allowed for each field in your object:
* String => `NSString`
* Number => `NSNumber`
* Bool => `NSNumber`
* Array => `NSArray`
* Object => `NSObject`
* Date => `NSDate`
* File => `PFFileObject`
* Pointer => other `PFObject`
* Relation => `PFRelation`
* Null => `NSNull`
Some examples:
<div class="language-toggle" markdown="1">
```objective_c
NSNumber *number = @42;
NSNumber *bool = @NO;
NSString *string = [NSString stringWithFormat:@"the number is %@", number];
NSDate *date = [NSDate date];
NSArray *array = @[string, number];
NSDictionary *dictionary = @{@"number": number, @"string": string};
NSNull *null = [NSNull null];
PFObject *pointer = [PFObject objectWithoutDataWithClassName:@"MyClassName" objectId:@"xyz"];
PFObject *bigObject = [PFObject objectWithClassName:@"BigObject"];
bigObject[@"myNumberKey"] = number;
bigObject[@"myBoolKey"] = bool;
bigObject[@"myStringKey"] = string;
bigObject[@"myDateKey"] = date;
bigObject[@"myArrayKey"] = array;
bigObject[@"myObjectKey"] = dictionary; // shows up as 'object' in the Data Browser
bigObject[@"anyKey"] = null; // this value can only be saved to an existing key
bigObject[@"myPointerKey"] = pointer; // shows up as Pointer MyClassName in the Data Browser
[bigObject saveInBackground];
let number = 42
let bool = false
let string = "the number is \(number)"
let date = NSDate()
let array = [string, number]
let dictionary = ["number": number, "string": string]
let null = NSNull()
let pointer = PFObject(objectWithoutDataWithClassName:"MyClassName", objectId: "xyz")
var bigObject = PFObject(className:"BigObject")
bigObject["myNumberKey"] = number
bigObject["myBooleanKey"] = bool
bigObject["myStringKey"] = string
bigObject["myDateKey"] = date
bigObject["myArrayKey"] = array
bigObject["myObjectKey"] = dictionary // shows up as 'object' in the Data Browser
bigObject["anyKey"] = null // this value can only be saved to an existing key
bigObject["myPointerKey"] = pointer // shows up as Pointer MyClassName in the Data Browser
bigObject.saveInBackground()
We do not recommend storing large pieces of binary data like images or documents on PFObject
. We recommend you use PFFileObject
s to store images, documents, and other types of files. You can do so by instantiating a PFFileObject
object and setting it on a field. See Files for more details.
For more information about how Parse handles data, check out our documentation on Data.
Parse is designed to get you up and running as quickly as possible. You can access all of your data using the PFObject
class and access any field with objectForKey:
or the []
subscripting operator. In mature codebases, subclasses have many advantages, including terseness, extensibility, and support for autocomplete. Subclassing is completely optional, but can transform this code:
PFObject *shield = [PFObject objectWithClassName:@"Armor"];
shield[@"displayName"] = @"Wooden Shield";
shield[@"fireProof"] = @NO;
shield[@"rupees"] = @50;
var shield = PFObject(className: "Armor")
shield["displayName"] = "Wooden Shield"
shield["fireProof"] = false
shield["rupees"] = 50
Into this:
Armor *shield = [Armor object];
shield.displayName = @"Wooden Shield";
shield.fireProof = NO;
shield.rupees = 50;
var shield = Armor()
shield.displayName = "Wooden Shield"
shield.fireProof = false
shield.rupees = 50
To create a subclass:
PFObject
which conforms to the PFSubclassing
protocol.parseClassName
and return the string you would pass to initWithClassName:
. This makes all future class name references unnecessary.Note: Objective-C developers should Import PFObject+Subclass
in your .m file. This implements all methods in PFSubclassing
beyond parseClassName
.
Adding custom properties and methods to your PFObject
subclass helps encapsulate logic about the class. With PFSubclassing
, you can keep all your logic about a subject in one place rather than using separate classes for business logic and storage/transmission logic.
PFObject
supports dynamic synthesizers just like NSManagedObject
. Declare a property as you normally would, but use @dynamic
rather than @synthesize
in your .m file. The following example creates a displayName
property in the Armor
class:
You can access the displayName property using armor.displayName
or [armor displayName]
and assign to it using armor.displayName = @"Wooden Shield"
or [armor setDisplayName:@"Wooden Sword"]
. Dynamic properties allow Xcode to provide autocomplete and catch typos.
NSNumber
properties can be implemented either as NSNumber
s or as their primitive counterparts. Consider the following example:
@property BOOL fireProof;
@property int rupees;
@NSManaged var fireProof: Boolean
@NSManaged var rupees: Int
In this case, game[@"fireProof"]
will return an NSNumber
which is accessed using boolValue
and game[@"rupees"]
will return an NSNumber
which is accessed using intValue
, but the fireProof
property is an actual BOOL
and the rupees
property is an actual int
. The dynamic getter will automatically extract the BOOL
or int
value and the dynamic setter will automatically wrap the value in an NSNumber
. You are free to use either format. Primitive property types are easier to use but NSNumber
property types support nil values more clearly.
If you need more complicated logic than simple property access, you can declare your own methods as well:
@dynamic iconFile;
- (UIImageView *)iconView {
PFImageView *view = [[PFImageView alloc] initWithImage:kPlaceholderImage];
view.file = self.iconFile;
[view loadInBackground];
return view;
}
@NSManaged var iconFile: PFFileObject!
func iconView() -> UIImageView {
let view = PFImageView(imageView: PlaceholderImage)
view.file = iconFile
view.loadInBackground()
return view
}
You should initialize new instances of subclassses with standard initialization methods. To create a new instance of an existing Parse object, use the inherited PFObject
class function objectWithoutDataWithObjectId:
, or create a new object and set the objectId property manually.
We’ve already seen how a PFQuery
with getObjectInBackgroundWithId:block:
can retrieve a single PFObject
from Parse. There are many other ways to retrieve data with PFQuery
- you can retrieve many objects at once, put conditions on the objects you wish to retrieve, cache queries automatically to avoid writing that code yourself, and more.
In many cases, getObjectInBackgroundWithId:block:
isn’t powerful enough to specify which objects you want to retrieve. The PFQuery
offers different ways to retrieve a list of objects rather than just a single object.
The general pattern is to create a PFQuery
, put conditions on it, and then retrieve a NSArray
of matching PFObject
s using either findObjectsInBackgroundWithBlock:
or findObjectsInBackgroundWithTarget:selector:
. For example, to retrieve scores with a particular playerName
, use the whereKey:equalTo:
method to constrain the value for a key.
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerName" equalTo:@"Dan Stemkoski"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
NSLog(@"Successfully retrieved %d scores.", objects.count);
// Do something with the found objects
for (PFObject *object in objects) {
NSLog(@"%@", object.objectId);
}
} else {
// Log details of the failure
NSLog(@"Error: %@ %@", error, [error userInfo]);
}
}];
let query = PFQuery(className:"GameScore")
query.whereKey("playerName", equalTo:"Sean Plott")
query.findObjectsInBackground { (objects: [PFObject]?, error: Error?) in
if let error = error {
// Log details of the failure
print(error.localizedDescription)
} else if let objects = objects {
// The find succeeded.
print("Successfully retrieved \(objects.count) scores.")
// Do something with the found objects
for object in objects {
print(object.objectId as Any)
}
}
}
Both findObjectsInBackgroundWithBlock:
and findObjectsInBackgroundWithTarget:selector:
work similarly in that they assure the network request is done without blocking, and run the block/callback in the main thread. There is a corresponding findObjects
method that blocks the calling thread, if you are already in a background thread:
// Only use this code if you are already running it in a background
// thread, or for testing purposes!
// Using PFQuery
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerName" equalTo:@"Dan Stemkoski"];
NSArray* scoreArray = [query findObjects];
// Using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"playerName = 'Dan Stemkosk'"];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
NSArray* scoreArray = [query findObjects];
// Only use this code if you are already running it in a background
// thread, or for testing purposes!
// Using PFQuery
let query = PFQuery(className: "GameScore")
query.whereKey("playerName", equalTo: "Dan Stemkoski")
let scoreArray = query.findObjects()
// Using NSPredicate
let predicate = NSPredicate(format:"playerName = 'Dan Stemkosk'")
let query = PFQuery(className: "GameScore", predicate: predicate)
let scoreArray = query.findObjects()
To get the most out of PFQuery
we recommend using its methods listed below to add constraints. However, if you prefer using NSPredicate
, a subset of the constraints can be specified by providing an NSPredicate
when creating your PFQuery
.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"playerName = 'Dan Stemkosk'"];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
let predicate = NSPredicate(format: "playerName = 'Dan Stemkosk'")
let query = PFQuery(className: "GameScore", predicate: predicate)
These features are supported:
=
, !=
, <
, >
, <=
, >=
, and BETWEEN
with a key and a constant.x IN {1, 2, 3}
.x IN SELF
.BEGINSWITH
expressions.AND
, OR
, and NOT
."key IN %@", subquery
.The following types of predicates are not supported:
ANY
, SOME
, ALL
, or NONE
.LIKE
, MATCHES
, CONTAINS
, or ENDSWITH
.OR
ed clauses.There are several ways to put constraints on the objects found by a PFQuery
. You can filter out objects with a particular key-value pair with whereKey:notEqualTo
:
// Using PFQuery
[query whereKey:@"playerName" notEqualTo:@"Michael Yabuti"];
// Using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat: @"playerName != 'Michael Yabuti'"];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
// Using PFQuery
query.whereKey("playerName", notEqualTo: "Michael Yabuti")
// Using NSPredicate
let predicate = NSPredicate(format:"playerName != 'Michael Yabuti'")
let query = PFQuery(className: "GameScore", predicate: predicate)
You can give multiple constraints, and objects will only be in the results if they match all of the constraints. In other words, it’s like an AND of constraints.
// Using PFQuery
[query whereKey:@"playerName" notEqualTo:@"Michael Yabuti"];
[query whereKey:@"playerAge" greaterThan:@18];
// Using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat: @"playerName != 'Michael Yabuti' AND playerAge > 18"];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
// Using PFQuery
query.whereKey("playerName", notEqualTo: "Michael Yabuti")
query.whereKey("playerAge", greaterThan: 18)
// Using NSPredicate
let predicate = NSPredicate(format:"playerName != 'Michael Yabuti' AND playerAge > 18")
let query = PFQuery(className: "GameScore", predicate: predicate)
You can limit the number of results by setting limit
. By default, results are limited to 100. In the old Parse hosted backend, the maximum limit was 1,000, but Parse Server removed that constraint:
query.limit = 10; // limit to at most 10 results
query.limit = 10 // limit to at most 10 results
If you want exactly one result, a more convenient alternative may be to use getFirstObject
or getFirstObjectInBackground
instead of using findObject
.
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerEmail" equalTo:@"[email protected]"];
[query getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!object) {
NSLog(@"The getFirstObject request failed.");
} else {
// The find succeeded.
NSLog(@"Successfully retrieved the object.");
}
}];
let query = PFQuery(className: "GameScore")
query.whereKey("playerEmail", equalTo: "[email protected]")
query.getFirstObjectInBackground { (object: PFObject?, error: Error?) in
if let error = error {
// The query failed
print(error.localizedDescription)
} else if let object = object {
// The query succeeded with a matching result
print(object)
} else {
// The query succeeded but no matching result was found
}
}
You can skip the first results by setting skip
. In the old Parse hosted backend, the maximum skip value was 10,000, but Parse Server removed that constraint. This can be useful for pagination:
query.skip = 10; // skip the first 10 results
query.skip = 10
For sortable types like numbers and strings, you can control the order in which results are returned:
// Sorts the results in ascending order by the score field
[query orderByAscending:@"score"];
// Sorts the results in descending order by the score field
[query orderByDescending:@"score"];
// Sorts the results in ascending order by the score field
query.order(byAscending: "score")
// Sorts the results in descending order by the score field
query.order(byDescending: "score")
You can add more sort keys to the query as follows:
// Sorts the results in ascending order by the score field if the previous sort keys are equal.
[query addAscendingOrder:@"score"];
// Sorts the results in descending order by the score field if the previous sort keys are equal.
[query addDescendingOrder:@"score"];
// Sorts the results in ascending order by the score field if the previous sort keys are equal.
query.addAscendingOrder("score")
// Sorts the results in descending order by the score field if the previous sort keys are equal.
query.addDescendingOrder("score")
For sortable types, you can also use comparisons in queries:
// Restricts to wins < 50
[query whereKey:@"wins" lessThan:@50];
// Or with NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"wins < 50"];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
// Restricts to wins <= 50
[query whereKey:@"wins" lessThanOrEqualTo:@50];
// Or with NSPredicate
predicate = [NSPredicate predicateWithFormat:@"wins <= 50"];
query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate]
// Restricts to wins > 50
[query whereKey:@"wins" greaterThan:@50];
// Or with NSPredicate
predicate = [NSPredicate predicateWithFormat:@"wins > 50"];
query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
// Restricts to wins >= 50
[query whereKey:@"wins" greaterThanOrEqualTo:@50];
// Or with NSPredicate
predicate = [NSPredicate predicateWithFormat:@"wins >= 50"];
query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
// Restricts to wins < 50
query.whereKey("wins", lessThan: 50)
// Or with NSPredicate
let predicate = NSPredicate(format: "wins < 50")
let query = PFQuery(className: "GameScore", predicate: predicate)
// Restricts to wins <= 50
query.whereKey("wins", lessThanOrEqualTo: 50)
// Or with NSPredicate
let predicate = NSPredicate(format: "wins <= 50")
let query = PFQuery(className: "GameScore", predicate: predicate)
// Restricts to wins > 50
query.whereKey("wins", greaterThan: 50)
// Or with NSPredicate
let predicate = NSPredicate(format: "wins > 50")
let query = PFQuery(className: "GameScore", predicate: predicate)
// Restricts to wins >= 50
query.whereKey("wins", greaterThanOrEqualTo: 50)
// Or with NSPredicate
let predicate = NSPredicate(format: "wins >= 50")
let query = PFQuery(className: "GameScore", predicate: predicate)
If you want to retrieve objects matching several different values, you can use whereKey:containedIn:
, providing an array of acceptable values. This is often useful to replace multiple queries with a single query. For example, if you want to retrieve scores made by any player in a particular list:
// Finds scores from any of Jonathan, Dario, or Shawn
// Using PFQuery
NSArray *names = @[@"Jonathan Walsh", @"Dario Wunsch", @"Shawn Simon"];
[query whereKey:@"playerName" containedIn:names];
// Using NSPredicate
NSArray *names = @[@"Jonathan Walsh", @"Dario Wunsch", @"Shawn Simon"];
NSPredicate *pred = [NSPredicate predicateWithFormat: @"playerName IN %@", names];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:pred];
// Finds scores from any of Jonathan, Dario, or Shawn
// Using PFQuery
let names = ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]
query.whereKey("playerName", containedIn: names)
// Using NSPredicate
let names = ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]
let predicate = NSPredicate(format: "playerName IN %@", names)
let query = PFQuery(className: "GameScore", predicate: predicate)
If you want to retrieve objects that do not match any of several values you can use whereKey:notContainedIn:
, providing an array of acceptable values. For example, if you want to retrieve scores from players besides those in a list:
// Finds scores from anyone who is neither Jonathan, Dario, nor Shawn
// Using PFQuery
NSArray *names = @[@"Jonathan Walsh", @"Dario Wunsch", @"Shawn Simon"];
[query whereKey:@"playerName" notContainedIn:names];
// Using NSPredicate
NSArray *names = @[@"Jonathan Walsh", @"Dario Wunsch", @"Shawn Simon"];
NSPredicate *pred = [NSPredicate predicateWithFormat: @"NOT (playerName IN %@)", names];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:pred];
// Finds scores from anyone who is neither Jonathan, Dario, nor Shawn
// Using PFQuery
let names = ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]
query.whereKey("playerName", notContainedIn: names)
// Using NSPredicate
let names = ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]
let predicate = NSPredicate(format: "NOT (playerName IN %@)", names)
let query = PFQuery(className: "GameScore", predicate: predicate)
If you want to retrieve objects that have a particular key set, you can use whereKeyExists
. Conversely, if you want to retrieve objects without a particular key set, you can use whereKeyDoesNotExist
.
// Finds objects that have the score set
[query whereKeyExists:@"score"];
// Or using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"score IN SELF"];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
// Finds objects that don't have the score set
[query whereKeyDoesNotExist:@"score"];
// Or using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (score IN SELF)"];
PFQuery *query = [PFQuery queryWithClassName:@"GameScore" predicate:predicate];
// Finds objects that have the score set
query.whereKeyExists("score")
// Or using NSPredicate
let predicate = NSPredicate(format: "score IN SELF")
let query = PFQuery(className: "GameScore", predicate: predicate)
// Finds objects that don't have the score set
query.whereKeyDoesNotExist("score")
// Or using NSPredicate
let predicate = NSPredicate(format: "NOT (score IN SELF)")
let query = PFQuery(className: "GameScore", predicate: predicate)
You can use the whereKey:matchesKey:inQuery:
method to get objects where a key matches the value of a key in a set of objects resulting from another query. For example, if you have a class containing sports teams and you store a user’s hometown in the user class, you can issue one query to find the list of users whose hometown teams have winning records. The query would look like:
PFQuery *teamQuery = [PFQuery queryWithClassName:@"Team"];
[teamQuery whereKey:@"winPct" greaterThan:@(0.5)];
PFQuery *userQuery = [PFQuery queryForUser];
[userQuery whereKey:@"hometown" matchesKey:@"city" inQuery:teamQuery];
[userQuery findObjectsInBackgroundWithBlock:^(NSArray *results, NSError *error) {
// results will contain users with a hometown team with a winning record
}];
let teamQuery = PFQuery(className:"Team")
teamQuery.whereKey("winPct", greaterThan:0.5)
let userQuery = PFUser.query()
userQuery?.whereKey("hometown", matchesKey: "city", in: teamQuery)
userQuery?.findObjectsInBackground(block: { (results: [PFObject]?, error: Error?) in
if let error = error {
// The query failed
print(error.localizedDescription)
} else {
// results will contain users with a hometown team with a winning record
}
})
Conversely, to get objects where a key does not match the value of a key in a set of objects resulting from another query, use whereKey:doesNotMatchKey:inQuery:
. For example, to find users whose hometown teams have losing records:
PFQuery *losingUserQuery = [PFQuery queryForUser];
[losingUserQuery whereKey:@"hometown" doesNotMatchKey:@"city" inQuery:teamQuery];
[losingUserQuery findObjectsInBackgroundWithBlock:^(NSArray *results, NSError *error) {
// results will contain users with a hometown team with a losing record
}];
let teamQuery = PFQuery(className:"Team")
teamQuery.whereKey("winPct", greaterThan:0.5)
let losingUserQuery = PFUser.query()
losingUserQuery?.whereKey("hometown", doesNotMatchKey:"city", in: teamQuery)
losingUserQuery?.findObjectsInBackground(block: { (results: [PFObject]?, error: Error?) in
if let error = error {
// The query failed
print(error.localizedDescription)
} else {
// results will contain users with a hometown team with a losing records
}
})
You can restrict the fields returned by calling selectKeys:
with an NSArray
of keys. To retrieve documents that contain only the score
and playerName
fields (and also special built-in fields such as objectId
, createdAt
, and updatedAt
):
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query selectKeys:@[@"playerName", @"score"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *results, NSError *error) {
// objects in results will only contain the playerName and score fields
}];
let query = PFQuery(className:"GameScore")
query.selectKeys(["playerName", "score"])
query.findObjectsInBackground { (objects: [PFObject]?, error: Error?) in
if let error = error {
// The query failed
print(error.localizedDescription)
} else {
// objects in results will only contain the playerName and score fields
}
}
The remaining fields can be fetched later by calling one of the fetchIfNeeded
variants on the returned objects:
PFObject *object = (PFObject*)results[0];
[object fetchIfNeededInBackgroundWithBlock:^(PFObject *object, NSError *error) {
// all fields of the object will now be available here.
}];
// Use array of PFObjects from earlier query
var object = objects?[0] as! PFObject
object.fetchInBackground(block: { (object: PFObject?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
// all fields of the object will now be available here.
}
})
For keys with an array type, you can find objects where the key’s array value contains 2 by:
// Find objects where the array in arrayKey contains 2.
// Using PFQuery
[query whereKey:@"arrayKey" equalTo:@2];
// Or using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"2 IN arrayKey"];
PFQuery *query = [PFQuery queryWithClassName:@"MyClass" predicate:predicate];
// Find objects where the array in arrayKey contains 2.
// Using PFQuery
query.whereKey("arrayKey", equalTo: 2)
// Or using NSPredicate
let predicate = NSPredicate(format: "2 IN arrayKey")
let query = PFQuery(className: "MyClass", predicate: predicate)
You can also find objects where the key’s array value contains each of the values 2, 3, and 4 with the following:
// Find objects where the array in arrayKey contains each of the
// elements 2, 3, and 4.
[query whereKey:@"arrayKey" containsAllObjectsInArray:@[@2, @3, @4]];
// Find objects where the array in arrayKey contains each of the
// elements 2, 3, and 4.
query.whereKey("arrayKey", containsAllObjectsIn:[2, 3, 4])
Use whereKey:hasPrefix:
to restrict to string values that start with a particular string. Similar to a MySQL LIKE operator, this is indexed so it is efficient for large datasets:
// Finds barbecue sauces that start with "Big Daddy".
// Using PFQuery
PFQuery *query = [PFQuery queryWithClassName:@"BarbecueSauce"];
[query whereKey:@"name" hasPrefix:@"Big Daddy's"];
// Using NSPredicate
NSPredicate *pred = [NSPredicate predicateWithFormat:@"name BEGINSWITH 'Big Daddy"];
PFQuery *query = [PFQuery queryWithClassName:@"BarbecueSauce" predicate:pred];
// Finds barbecue sauces that start with "Big Daddy".
// Using PFQuery
let query = PFQuery(className: "BarbecueSauce")
query.whereKey("name", hasPrefix: "Big Daddy's")
// Using NSPredicate
let pred = NSPredicate(format: "name BEGINSWITH 'Big Daddy")
let query = PFQuery(className: "BarbecueSauce", predicate: predicate)
The above example will match any BarbecueSauce
objects where the value in the “name” String key starts with “Big Daddy’s”. For example, both “Big Daddy’s” and “Big Daddy’s BBQ” will match, but “big daddy’s” or “BBQ Sauce: Big Daddy’s” will not.
Queries that have regular expression constraints are very expensive. Refer to the Performance Guide for more details.
You can use whereKey:matchesText
for efficient search capabilities. Text indexes are automatically created for you. Your strings are turned into tokens for fast searching.
Note: Full Text Search can be resource intensive. Ensure the cost of using indexes is worth the benefit, see storage requirements & performance costs of text indexes..
Requires Parse Server 2.5.0+
PFQuery *query = [PFQuery queryWithClassName:@"BarbecueSauce"];
[query whereKey:@"name" matchesText:@"bbq"];
let query = PFQuery(className: "BarbecueSauce")
query.whereKey("name", matchesText: "bbq")
The above example will match any BarbecueSauce
objects where the value in the “name” String key contains “bbq”. For example, both “Big Daddy’s BBQ”, “Big Daddy’s bbq” and “Big BBQ Daddy” will match.
// You can sort by weight / rank. orderByAscending and selectKeys
PFQuery *query = [PFQuery queryWithClassName:@"BarbecueSauce"];
[query whereKey:@"name" matchesText:@"bbq"];
[query orderByAscending:@"$score"];
[query selectKeys:@[@"$score"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
for (PFObject *object in objects) {
NSLog(@"Successfully retrieved %d weight / rank.", object[@"$score"]);
}
} else {
// Log details of the failure
NSLog(@"Error: %@ %@", error, [error userInfo]);
}
}];
let query = PFQuery(className: "BarbecueSauce")
query.whereKey("name", matchesText: "bbq")
query.order(byAscending: "$score")
query.selectKeys(["$score"])
query.findObjectsInBackground { (objects: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else if let objects = objects {
objects.forEach { (object) in
print("Successfully retrieved \(String(describing: object["$score"])) weight / rank.");
}
}
}
For Case or Diacritic Sensitive search, please use the REST API.
There are several ways to issue queries for relational data. If you want to retrieve objects where a field matches a particular PFObject
, you can use whereKey:equalTo:
just like for other data types. For example, if each Comment
has a Post
object in its post
field, you can fetch comments for a particular Post
:
// Assume PFObject *myPost was previously created.
// Using PFQuery
PFQuery *query = [PFQuery queryWithClassName:@"Comment"];
[query whereKey:@"post" equalTo:myPost];
[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
// comments now contains the comments for myPost
}];
// Using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"post = %@", myPost];
PFQuery *query = [PFQuery queryWithClassName:@"Comment" predicate:predicate];
[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
// comments now contains the comments for myPost
}];
// Assume PFObject *myPost was previously created.
// Using PFQuery
let query = PFQuery(className: "Comment")
query.whereKey("post", equalTo: myPost)
query.findObjectsInBackground { (comments: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
// comments now contains the comments for myPost
}
}
// Using NSPredicate
let predicate = NSPredicate(format: "post = %@", myPost)
let query = PFQuery(className: "Comment", predicate: predicate)
query.findObjectsInBackground { (comments: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
// comments now contains the comments for myPost
}
}
You can also do relational queries by objectId
:
// Using PFQuery
[query whereKey:@"post" equalTo:[PFObject objectWithoutDataWithClassName:@"Post" objectId:@"1zEcyElZ80"]];
// Using NSPredicate
[NSPredicate predicateWithFormat:@"post = %@",
[PFObject objectWithoutDataWithClassName:@"Post" objectId:@"1zEcyElZ80"]];
// Using PFQuery
query.whereKey("post", equalTo: PFObject(withoutDataWithClassName: "Post", objectId: "1zEcyElZ80"))
// Using NSPredicate
NSPredicate(format: "post = %@", PFObject(withoutDataWithClassName: "Post", objectId: "1zEcyElZ80"))
If you want to retrieve objects where a field contains a PFObject
that match a different query, you can use whereKey:matchesQuery
. In order to find comments for posts with images, you can do:
// Using PFQuery
PFQuery *innerQuery = [PFQuery queryWithClassName:@"Post"];
[innerQuery whereKeyExists:@"image"];
PFQuery *query = [PFQuery queryWithClassName:@"Comment"];
[query whereKey:@"post" matchesQuery:innerQuery];
[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
// comments now contains the comments for posts with images
}];
// Using NSPredicate
NSPredicate *innerPred = [NSPredicate predicateWithFormat:@"image IN SELF"];
PFQuery *innerQuery = [PFQuery queryWithClassName:@"Post" predicate:innerPred];
NSPredicate *pred = [NSPredicate predicateWithFormat:@"post IN %@", innerQuery];
PFQuery *query = [PFQuery queryWithClassName:@"Comment" predicate:pred];
[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
// comments now contains the comments for posts with images
}];
// Using PFQuery
let innerQuery = PFQuery(className: "Post")
innerQuery.whereKeyExists("image")
let query = PFQuery(className: "Comment")
query.whereKey("post", matchesQuery: innerQuery)
query.findObjectsInBackground { (comments: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
// comments now contains the comments for posts with images
}
}
// Using NSPredicate
let innerPred = NSPredicate(format: "image IN SELF")
let innerQuery = PFQuery(className: "Post", predicate: innerPred)
let pred = NSPredicate(format: "post IN %@", innerQuery)
let query = PFQuery(className: "Comment", predicate: pred)
query.findObjectsInBackground { (comments: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
// comments now contains the comments for posts with images
}
}
If you want to retrieve objects where a field contains a PFObject
that does not match a different query, you can use whereKey:doesNotMatchQuery
. In order to find comments for posts without images, you can do:
// Using PFQuery
PFQuery *innerQuery = [PFQuery queryWithClassName:@"Post"];
[innerQuery whereKeyExists:@"image"];
PFQuery *query = [PFQuery queryWithClassName:@"Comment"];
[query whereKey:@"post" doesNotMatchQuery:innerQuery];
[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
// comments now contains the comments for posts without images
}];
// Using NSPredicate
NSPredicate *innerPred = [NSPredicate predicateWithFormat:@"image IN SELF"];
PFQuery *innerQuery = [PFQuery queryWithClassName:@"Post" predicate:innerPred];
NSPredicate *pred = [NSPredicate predicateWithFormat:@"NOT (post IN %@)", innerQuery];
PFQuery *query = [PFQuery queryWithClassName:@"Comment" predicate:pred];
[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
// comments now contains the comments for posts without images
}];
// Using PFQuery
let innerQuery = PFQuery(className: "Post")
innerQuery.whereKeyExists("image")
let query = PFQuery(className: "Comment")
query.whereKey("post", doesNotMatch: innerQuery)
query.findObjectsInBackground { (comments: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
// comments now contains the comments for posts without images
}
}
// Using NSPredicate
let innerPred = NSPredicate(format: "image IN SELF")
let innerQuery = PFQuery(className: "Post", predicate: innerPred)
let pred = NSPredicate(format: "NOT (post IN %@)", innerQuery)
let query = PFQuery(className: "Comment", predicate: pred)
query.findObjectsInBackground { (comments: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
// comments now contains the comments for posts without images
}
}
In some situations, you want to return multiple types of related objects in one query. You can do this with the includeKey:
method. For example, let’s say you are retrieving the last ten comments, and you want to retrieve their related posts at the same time:
PFQuery *query = [PFQuery queryWithClassName:@"Comment"];
// Retrieve the most recent ones
[query orderByDescending:@"createdAt"];
// Only retrieve the last ten
query.limit = 10;
// Include the post data with each comment
[query includeKey:@"post"];
[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
// Comments now contains the last ten comments, and the "post" field
// has been populated. For example:
for (PFObject *comment in comments) {
// This does not require a network access.
PFObject *post = comment[@"post"];
NSLog(@"retrieved related post: %@", post);
}
}];
let query = PFQuery(className:"Comment")
// Retrieve the most recent ones
query.order(byDescending: "createdAt")
// Only retrieve the last ten
query.limit = 10
// Include the post data with each comment
query.includeKey("post")
query.findObjectsInBackground { (comments: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else if let comments = comments {
// Comments now contains the last ten comments, and the "post" field
// has been populated. For example:
for comment in comments {
// This does not require a network access.
let post = comment["post"] as? PFObject
print("retrieved related post: \(String(describing: post))")
}
}
}
You can also do multi level includes using dot notation. If you wanted to include the post for a comment and the post’s author as well you can do:
[query includeKey:@"post.author"];
query.includeKey("post.author")
You can issue a query with multiple fields included by calling includeKey:
multiple times. This functionality also works with PFQuery helpers like getFirstObject
and getObjectInBackground
If you have enabled the local datastore by calling [Parse enableLocalDatastore]
before your call to [Parse setApplicationId:clientKey:]
, then you can also query against the objects stored locally on the device. To do this, call the fromLocalDatastore
method on the query.
[query fromLocalDatastore];
[[query findObjectsInBackground] continueWithBlock:^id(BFTask *task) {
if (!task.error) {
// There was an error.
return task;
}
// Results were successfully found from the local datastore.
return task;
}];
let query = PFQuery(className:"Comment")
query.fromLocalDatastore()
query.findObjectsInBackground().continueWith { (task: BFTask<NSArray>) -> Any? in
if task.error != nil {
// There was an error.
return task
}
// Results were successfully found from the local datastore.
return task
}
You can query from the local datastore using exactly the same kinds of queries you use over the network. The results will include every object that matches the query that’s been pinned to your device. The query even takes into account any changes you’ve made to the object that haven’t yet been saved to the cloud. For example, if you call deleteEventually
, on an object, it will no longer be returned from these queries.
It’s often useful to cache the result of a query on disk. This lets you show data when the user’s device is offline, or when the app has just started and network requests have not yet had time to complete. Parse takes care of automatically flushing the cache when it takes up too much space.
The default query behavior doesn’t use the cache, but you can enable caching by setting query.cachePolicy
. For example, to try the network and then fall back to cached data if the network is not available:
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
query.cachePolicy = kPFCachePolicyNetworkElseCache;
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// Results were successfully found, looking first on the
// network and then on disk.
} else {
// The network was inaccessible and we have no cached data for
// this query.
}
}];
let query = PFQuery(className:"GameScore")
query.cachePolicy = .cacheElseNetwork
query.findObjectsInBackground { (objects: [PFObject]?, error: Error?) in
if let error = error {
// The network was inaccessible and we have no cached data for
// this query.
print(error.localizedDescription)
} else {
// Results were successfully found, looking first on the
// network and then on disk.
}
}
Parse provides several different cache policies:
IgnoreCache
: The query does not load from the cache or save results to the cache. IgnoreCache
is the default cache policy.CacheOnly
: The query only loads from the cache, ignoring the network. If there are no cached results, that causes a PFError
.NetworkOnly
: The query does not load from the cache, but it will save results to the cache.CacheElseNetwork
: The query first tries to load from the cache, but if that fails, it loads results from the network. If neither cache nor network succeed, there is a PFError
.NetworkElseCache
: The query first tries to load from the network, but if that fails, it loads results from the cache. If neither network nor cache succeed, there is a PFError
.CacheThenNetwork
: The query first loads from the cache, then loads from the network. In this case, the callback will actually be called twice - first with the cached results, then with the network results. Since it returns two results at different times, this cache policy cannot be used synchronously with findObjects
.If you need to control the cache’s behavior, you can use methods provided in PFQuery to interact with the cache. You can do the following operations on the cache:
BOOL isInCache = [query hasCachedResult];
let isInCache = query.hasCachedResult
[query clearCachedResult];
query.clearCachedResult()
[PFQuery clearAllCachedResults];
PFQuery.clearAllCachedResults()
Query caching also works with PFQuery helpers including getFirstObject
and getObjectInBackground
.
Note: In the old Parse hosted backend, count queries were rate limited to a maximum of 160 requests per minute. They also returned inaccurate results for classes with more than 1,000 objects. But, Parse Server has removed both constraints and can count objects well above 1,000.
If you just need to count how many objects match a query, but you do not need to retrieve the objects that match, you can use countObjects
instead of findObjects
. For example, to count how many games have been played by a particular player:
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playername" equalTo:@"Sean Plott"];
[query countObjectsInBackgroundWithBlock:^(int count, NSError *error) {
if (!error) {
// The count request succeeded. Log the count
NSLog(@"Sean has played %d games", count);
} else {
// The request failed
}
}];
let query = PFQuery(className:"GameScore")
query.whereKey("playerName", equalTo:"Sean Plott")
query.countObjectsInBackground { (count: Int32, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
print("Sean has played \(count) games")
}
}
If you want to block the calling thread, you can also use the synchronous countObjects
method.
If you want to find objects that match one of several queries, you can use orQueryWithSubqueries:
method. For instance, if you want to find players with either have a lot of wins or a few wins, you can do:
PFQuery *lotsOfWins = [PFQuery queryWithClassName:@"Player"];
[lotsOfWins whereKey:@"wins" greaterThan:@150];
PFQuery *fewWins = [PFQuery queryWithClassName:@"Player"];
[fewWins whereKey:@"wins" lessThan:@5];
PFQuery *query = [PFQuery orQueryWithSubqueries:@[fewWins,lotsOfWins]];
[query findObjectsInBackgroundWithBlock:^(NSArray *results, NSError *error) {
// results contains players with lots of wins or only a few wins.
}];
let lotsOfWins = PFQuery(className:"Player")
lotsOfWins.whereKey("wins", greaterThan:150)
let fewWins = PFQuery(className:"Player")
fewWins.whereKey("wins", lessThan:5)
let query = PFQuery.orQuery(withSubqueries: [lotsOfWins, fewWins])
query.findObjectsInBackground { (results: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else {
// results contains players with lots of wins or only a few wins.
}
}
You can add additional constraints to the newly created PFQuery
that act as an ‘and’ operator.
Note that we do not, however, support GeoPoint or non-filtering constraints (e.g. nearGeoPoint
, withinGeoBox...:
, limit
, skip
, orderBy...:
, includeKey:
) in the subqueries of the compound query.
You can get a query for objects of a particular subclass using the class method query
. The following example queries for armors that the user can afford:
PFQuery *query = [Armor query];
[query whereKey:@"rupees" lessThanOrEqualTo:[PFUser currentUser][@"rupees"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
Armor *firstArmor = [objects firstObject];
// ...
}
}];
let query = Armor.query()
query.whereKey("rupees", lessThanOrEqualTo: PFUser.current()?["rupees"] as Any)
query.findObjectsInBackground { (objects: [PFObject]?, error: Error?) in
if let error = error {
// The request failed
print(error.localizedDescription)
} else if let objects = objects as? [Armor], let firstArmor = objects.first {
//...
}
}
At the core of many apps, there is a notion of user accounts that lets users access their information in a secure manner. We provide a specialized user class called PFUser
that automatically handles much of the functionality required for user account management.
With this class, you’ll be able to add user account functionality in your app.
PFUser
is a subclass of PFObject
and has all the same features, such as flexible schema, automatic persistence, and a key value interface. All the methods that are on PFObject
also exist in PFUser
. The difference is that PFUser has some special additions specific to user accounts.
PFUser
PropertiesPFUser
has several properties that set it apart from PFObject
:
We’ll go through each of these in detail as we run through the various use cases for users. Keep in mind that if you set username
and email
through these properties, you do not need to set it using the setObject:forKey:
method — this is set for you automatically.
The first thing your app will do is probably ask the user to sign up. The following code illustrates a typical sign up:
- (void)myMethod {
PFUser *user = [PFUser user];
user.username = @"my name";
user.password = @"my pass";
user.email = @"[email protected]";
// 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 { NSString *errorString = [error userInfo][@"error"]; // Show the errorString somewhere and let the user try again.
}
}];
}
func myMethod() {
var user = PFUser()
user.username = "myUsername"
user.password = "myPassword"
user.email = "[email protected]"
// other fields can be set just like with PFObject
user["phone"] = "415-392-0202"
user.signUpInBackground {
(succeeded: Bool, error: Error?) -> Void in
if let error = error {
let errorString = error.localizedDescription
// Show the errorString somewhere and let the user try again.
} else {
// Hooray! Let them use the app now.
}
}
}
This call will asynchronously create a new user in your Parse App. Before it does this, it also checks to make sure that both the username and email are unique. Also, it securely hashes the password in the cloud using bcrypt. We never store passwords in plaintext, nor will we ever transmit passwords back to the client in plaintext.
Note that we used the signUp
method, not the save
method. New PFUser
s should always be created using the signUp
method. Subsequent updates to a user can be done by calling save
.
The signUp
method comes in various flavors, with the ability to pass back errors, and also synchronous versions. As usual, we highly recommend using the asynchronous versions when possible, so as not to block the UI in your app. You can read more about these specific methods in our API docs.
If a signup isn’t successful, you should read the error object that is returned. The most likely case is that the username or email has already been taken by another user. You should clearly communicate this to your users, and ask them try a different username.
You are free to use an email address as the username. Simply ask your users to enter their email, but fill it in the username property — PFUser
will work as normal. We’ll go over how this is handled in the reset password section.
Of course, after you allow users to sign up, you need to let them log in to their account in the future. To do this, you can use the class method logInWithUsernameInBackground:password:
.
[PFUser logInWithUsernameInBackground:@"myname" password:@"mypass"
block:^(PFUser *user, NSError *error) {
if (user) {
// Do stuff after successful login.
} else {
// The login failed. Check error to see why.
}
}];
PFUser.logInWithUsername(inBackground:"myname", password:"mypass") {
(user: PFUser?, error: Error?) -> Void in
if user != nil {
// Do stuff after successful login.
} else {
// The login failed. Check error to see why.
}
}
Enabling email verification in an application’s settings allows the application to reserve part of its experience for users with confirmed email addresses. Email verification adds the emailVerified
key to the PFUser
object. When a PFUser
’s email
is set or modified, emailVerified
is set to false
. Parse then emails the user a link which will set emailVerified
to true
.
There are three emailVerified
states to consider:
true
- the user confirmed his or her email address by clicking on the link Parse emailed them. PFUsers
can never have a true
value when the user account is first created.false
- at the time the PFUser
object was last refreshed, the user had not confirmed his or her email address. If emailVerified
is false
, consider calling refresh:
on the PFUser
.PFUser
was created when email verification was off or the PFUser
does not have an email
.It would be bothersome if the user had to log in every time they open your app. You can avoid this by using the cached currentUser
object.
Whenever you use any signup or login methods, the user is cached on disk. You can treat this cache as a session, and automatically assume the user is logged in:
PFUser *currentUser = [PFUser currentUser];
if (currentUser) {
// do stuff with the user
} else {
// show the signup or login screen
}
var currentUser = PFUser.current()
if currentUser != nil {
// Do stuff with the user
} else {
// Show the signup or login screen
}
You can clear the current user by logging them out:
[PFUser logOut];
PFUser *currentUser = [PFUser currentUser]; // this will now be nil
PFUser.logOut()
var currentUser = PFUser.current() // this will now be nil
Being able to associate data and objects with individual users is highly valuable, but sometimes you want to be able to do this without forcing a user to specify a username and password.
An anonymous user is a user that can be created without a username and password but still has all of the same capabilities as any other PFUser
. After logging out, an anonymous user is abandoned, and its data is no longer accessible.
You can create an anonymous user using PFAnonymousUtils
:
[PFAnonymousUtils logInWithBlock:^(PFUser *user, NSError *error) {
if (error) {
NSLog(@"Anonymous login failed.");
} else {
NSLog(@"Anonymous user logged in.");
}
}];
PFAnonymousUtils.logInWithBlock {
(user: PFUser?, error: NSError?) -> Void in
if error != nil || user == nil {
print("Anonymous login failed.")
} else {
print("Anonymous user logged in.")
}
}
You can convert an anonymous user into a regular user by setting the username and password, then calling signUp
, or by logging in or linking with a service like Facebook or Twitter. The converted user will retain all of its data. To determine whether the current user is an anonymous user, you can check PFAnonymousUtils isLinkedWithUser
:
if ([PFAnonymousUtils isLinkedWithUser:[PFUser currentUser]]) {
[self enableSignUpButton];
} else {
[self enableLogOutButton];
}
if PFAnonymousUtils.isLinkedWithUser(PFUser.currentUser()) {
self.enableSignUpButton()
} else {
self.enableLogOutButton()
}
Anonymous users can also be automatically created for you without requiring a network request, so that you can begin working with your user immediately when your application starts. When you enable automatic anonymous user creation at application startup, [PFUser currentUser]
will never be nil
. The user will automatically be created in the cloud the first time the user or any object with a relation to the user is saved. Until that point, the user’s object ID will be nil
. Enabling automatic user creation makes associating data with your users painless. For example, in your application:didFinishLaunchingWithOptions:
function, you might write:
[PFUser enableAutomaticUser];
[[PFUser currentUser] incrementKey:@"RunCount"];
[[PFUser currentUser] saveInBackground];
PFUser.enableAutomaticUser()
PFUser.currentUser().incrementKey("RunCount")
PFUser.currentUser().saveInBackground()
If you’ve created your own authentication routines, or otherwise logged in a user on the server side, you can now pass the session token to the client and use the become
method. This method will ensure the session token is valid before setting the current user.
[PFUser becomeInBackground:@"session-token-here" block:^(PFUser *user, NSError *error) {
if (error) {
// The token could not be validated.
} else {
// The current user is now set to user.
}
}];
PFUser.becomeInBackground("session-token-here", {
(user: PFUser?, error: NSError?) -> Void in
if error != nil {
// The token could not be validated.
} else {
// The current user is now set to user.
}
})
The PFUser
class is secured by default. Data stored in a PFUser
can only be modified by that user. By default, the data can still be read by any client. Thus, some PFUser
objects are authenticated and can be modified, whereas others are read-only.
Specifically, you are not able to invoke any of the save
or delete
methods unless the PFUser
was obtained using an authenticated method, like logIn
or signUp
. This ensures that only the user can alter their own data.
The following illustrates this security policy:
PFUser *user = [PFUser logInWithUsername:@"my_username" password:@"my_password"];
user.username = "my_new_username"; // attempt to change username
[user save]; // This succeeds, since the user was authenticated on the device
// Get the user from a non-authenticated method
PFQuery *query = [PFUser query];
PFUser *userAgain = (PFUser *)[query getObjectWithId:user.objectId];
userAgain.username = "another_username";
// This will throw an exception, since the PFUser is not authenticated
[userAgain save];
var user = PFUser.logInWithUsername("my_username", password:"my_password")
user.username = "my_new_username" // attempt to change username
user.save() // This succeeds, since the user was authenticated on the device
// Get the user from a non-authenticated method
var query = PFUser.query()
var userAgain = query.getObjectWithId(user.objectId) as PFUser
userAgain.username = "another_username"
// This will crash, since the PFUser is not authenticated
userAgain.save()
The PFUser
obtained from currentUser
will always be authenticated.
If you need to check if a PFUser
is authenticated, you can invoke the isAuthenticated
method. You do not need to check isAuthenticated
with PFUser
objects that are obtained via an authenticated method.
The same security model that applies to the PFUser
can be applied to other objects. For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object. To support this type of security, each object has an access control list, implemented by the PFACL
class.
The simplest way to use a PFACL
is to specify that an object may only be read or written by a single user. To create such an object, there must first be a logged in PFUser
. Then, the ACLWithUser
method generates a PFACL
that limits access to that user. An object’s ACL is updated when the object is saved, like any other property. Thus, to create a private note that can only be accessed by the current user:
PFObject *privateNote = [PFObject objectWithClassName:@"Note"];
privateNote[@"content"] = @"This note is private!";
privateNote.ACL = [PFACL ACLWithUser:[PFUser currentUser]];
[privateNote saveInBackground];
var privateNote = PFObject(className:"Note")
privateNote["content"] = "This note is private!"
privateNote.ACL = PFACL.ACLWithUser(PFUser.currentUser())
privateNote.saveInBackground()
This note will then only be accessible to the current user, although it will be accessible to any device where that user is signed in. This functionality is useful for applications where you want to enable access to user data across multiple devices, like a personal todo list.
Permissions can also be granted on a per-user basis. You can add permissions individually to a PFACL
using setReadAccess:forUser:
and setWriteAccess:forUser:
. For example, let’s say you have a message that will be sent to a group of several users, where each of them have the rights to read and delete that message:
PFObject *groupMessage = [PFObject objectWithClassName:@"Message"];
PFACL *groupACL = [PFACL ACL];
// userList is an NSArray with the users we are sending this message to.
for (PFUser *user in userList) {
[groupACL setReadAccess:YES forUser:user];
[groupACL setWriteAccess:YES forUser:user];
}
groupMessage.ACL = groupACL;
[groupMessage saveInBackground];
var groupMessage = PFObject(className:"Message")
var groupACL = PFACL.ACL()
// userList is an NSArray with the users we are sending this message to.
for (user : PFUser in userList) {
groupACL.setReadAccess(true, forUser:user)
groupACL.setWriteAccess(true, forUser:user)
}
groupMessage.ACL = groupACL
groupMessage.saveInBackground()
You can also grant permissions to all users at once using setPublicReadAccess:
and setPublicWriteAccess:
. This allows patterns like posting comments on a message board. For example, to create a post that can only be edited by its author, but can be read by anyone:
PFObject *publicPost = [PFObject objectWithClassName:@"Post"];
PFACL *postACL = [PFACL ACLWithUser:[PFUser currentUser]];
[postACL setPublicReadAccess:YES];
publicPost.ACL = postACL;
[publicPost saveInBackground];
var publicPost = PFObject(className:"Post")
var postACL = PFACL.ACLWithUser(PFUser.currentUser())
postACL.setPublicReadAccess(true)
publicPost.ACL = postACL
publicPost.saveInBackground()
To help ensure that your users’ data is secure by default, you can set a default ACL to be applied to all newly-created PFObjects
:
[PFACL setDefaultACL:defaultACL withAccessForCurrentUser:YES];
PFACL.setDefaultACL(defaultACL, withAccessForCurrentUser:true)
In the code above, the second parameter to setDefaultACL tells Parse to ensure that the default ACL assigned at the time of object creation allows read and write access to the current user at that time. Without this setting, you would need to reset the defaultACL every time a user logs in or out so that the current user would be granted access appropriately. With this setting, you can ignore changes to the current user until you explicitly need to grant different kinds of access.
Default ACLs make it easy to create apps that follow common access patterns. An application like Twitter, for example, where user content is generally visible to the world, might set a default ACL such as:
PFACL *defaultACL = [PFACL ACL];
[defaultACL setPublicReadAccess:YES];
[PFACL setDefaultACL:defaultACL withAccessForCurrentUser:YES];
var defaultACL = PFACL.ACL()
defaultACL.setPublicReadAccess(true)
PFACL.setDefaultACL(defaultACL, withAccessForCurrentUser:true)
For an app like Dropbox, where a user’s data is only accessible by the user itself unless explicit permission is given, you would provide a default ACL where only the current user is given access:
[PFACL setDefaultACL:[PFACL ACL] withAccessForCurrentUser:YES];
PFACL.setDefaultACL(PFACL.ACL(), withAccessForCurrentUser:true)
For an application that logs data to Parse but doesn’t provide any user access to that data, you would deny access to the current user while providing a restrictive ACL:
[PFACL setDefaultACL:[PFACL ACL] withAccessForCurrentUser:NO];
PFACL.setDefaultACL(PFACL.ACL(), withAccessForCurrentUser:false)
Operations that are forbidden, such as deleting an object that you do not have write access to, result in a kPFErrorObjectNotFound
error code. For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which object ids do not exist at all.
It’s a fact that as soon as you introduce passwords into a system, users will forget them. In such cases, our library provides a way to let them securely reset their password.
To kick off the password reset flow, ask the user for their email address, and call:
[PFUser requestPasswordResetForEmailInBackground:@"[email protected]"];
PFUser.requestPasswordResetForEmail(inBackground:"[email protected]")
This will attempt to match the given email with the user’s email or username field, and will send them a password reset email. By doing this, you can opt to have users use their email as their username, or you can collect it separately and store it in the email field.
The flow for password reset is as follows:
Note that the messaging in this flow will reference your app by the name that you specified when you created this app on Parse.
To query for users, you need to use the special user query:
PFQuery *query = [PFUser query];
[query whereKey:@"gender" equalTo:@"female"]; // find all the women
NSArray *girls = [query findObjects];
var query = PFUser.query()
query.whereKey("gender", equalTo:"female")
var girls = query.findObjects()
In addition, you can use getUserObjectWithId:objectId
to get a PFUser
by id.
Associations involving a PFUser
work right out of the box. For example, let’s say you’re making a blogging app. To store a new post for a user and retrieve all their posts:
PFUser *user = [PFUser currentUser];
// Make a new post
PFObject *post = [PFObject objectWithClassName:@"Post"];
post[@"title"] = @"My New Post";
post[@"body"] = @"This is some great content.";
post[@"user"] = user;
[post save];
// Find all posts by the current user
PFQuery *query = [PFQuery queryWithClassName:@"Post"];
[query whereKey:@"user" equalTo:user];
NSArray *usersPosts = [query findObjects];
var user = PFUser.currentUser()
// Make a new post
var post = PFObject(className:"Post")
post["title"] = "My New Post"
post["body"] = "This is some great content."
post["user"] = user
post.save()
Parse provides an easy way to integrate Facebook with your application. The Facebook SDK can be used with our SDK, and is integrated with the PFUser
class to make linking your users to their Facebook identities easy.
Using our Facebook integration, you can associate an authenticated Facebook user with a PFUser
. With just a few lines of code, you’ll be able to provide a “log in with Facebook” option in your app, and be able to save the user’s data to Parse.
Note: Parse SDK is compatible both with Facebook SDK 3.x and 4.x for iOS. These instructions are for Facebook SDK 4.x.
To start using Facebook with Parse, you need to:
ParseFacebookUtils.framework
to your Xcode project, by dragging it into your project folder target.There’s also two code changes you’ll need to make. First, add the following to your application:didFinishLaunchingWithOptions:
method, after you’ve initialized the Parse SDK.
// AppDelegate.m
#import <FBSDKCoreKit/FBSDKCoreKit.h>
#import <ParseFacebookUtilsV4/PFFacebookUtils.h>
@implementation AppDelegate
- (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[Parse setApplicationId:@"parseAppId" clientKey:@"parseClientKey"];
[PFFacebookUtils initializeFacebookWithApplicationLaunchOptions:launchOptions];
}
import FBSDKCoreKit
import Parse
// AppDelegate.swift
func application(application: UIApplicatiofunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initialize Parse.
let parseConfig = ParseClientConfiguration {
$0.applicationId = "parseAppId"
$0.clientKey = "parseClientKey"
$0.server = "parseServerUrlString"
}
Parse.initialize(with: parseConfig)
PFFacebookUtils.initializeFacebook(applicationLaunchOptions: launchOptions)
}
Next, add the following handlers in your app delegate.
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
return [[FBSDKApplicationDelegate sharedInstance] application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
[FBSDKAppEvents activateApp];
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
return FBSDKApplicationDelegate.sharedInstance().application(
application,
open: url,
sourceApplication: sourceApplication,
annotation: annotation
)
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return FBSDKApplicationDelegate.sharedInstance().application(
app,
open: url,
sourceApplication: options[.sourceApplication] as? String,
annotation: options[.annotation]
)
}
//Make sure it isn't already declared in the app delegate (possible redefinition of func error)
func applicationDidBecomeActive(_ application: UIApplication) {
FBSDKAppEvents.activateApp()
}
There are two main ways to use Facebook with your Parse users: (1) to log in (or sign up) as a Facebook user and creating a PFUser
, or (2) linking Facebook to an existing PFUser
.
PFUser
provides a way to allow your users to log in or sign up through Facebook. This is done by using the logInInBackgroundWithReadPermissions
method like so:
[PFFacebookUtils logInInBackgroundWithReadPermissions:permissions block:^(PFUser *user, NSError *error) {
if (!user) {
NSLog(@"Uh oh. The user cancelled the Facebook login.");
} else if (user.isNew) {
NSLog(@"User signed up and logged in through Facebook!");
} else {
NSLog(@"User logged in through Facebook!");
}
}];
PFFacebookUtils.logInInBackground(withReadPermissions: permissions) {
(user: PFUser?, error: Error?) in
if let user = user {
if user.isNew {
print("User signed up and logged in through Facebook!")
} else {
print("User logged in through Facebook!")
}
} else {
print("Uh oh. The user cancelled the Facebook login.")
}
}
When this code is run, the following happens:
handleOpenURL
.PFUser
. If no PFUser
exists with the same Facebook ID, then a new PFUser
is created.The permissions argument is an array of strings that specifies what permissions your app requires from the Facebook user. These permissions must only include read permissions. The PFUser
integration doesn’t require any permissions to work out of the box. Read more permissions on Facebook’s developer guide.
To acquire publishing permissions for a user so that your app can, for example, post status updates on their behalf, you must call [PFFacebookUtils logInInBackgroundWithPublishPermissions:]
:
[PFFacebookUtils logInInBackgroundWithPublishPermissions:@[ @"publish_actions" ] block:^(PFUser *user, NSError *error) {
if (!user) {
NSLog(@"Uh oh. The user cancelled the Facebook login.");
} else {
NSLog(@"User now has publish permissions!");
}
}];
PFFacebookUtils.logInInBackgroundWithPublishPermissions(["publish_actions"], {
(user: PFUser?, error: NSError?) -> Void in
if user != nil {
// Your app now has publishing permissions for the user
}
})
If you want to associate an existing PFUser
to a Facebook account, you can link it like so:
if (![PFFacebookUtils isLinkedWithUser:user]) {
[PFFacebookUtils linkUserInBackground:user withReadPermissions:nil block:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Woohoo, user is linked with Facebook!");
}
}];
}
if !PFFacebookUtils.isLinkedWithUser(user) {
PFFacebookUtils.linkUserInBackground(user, withReadPermissions: nil, {
(succeeded: Bool?, error: NSError?) -> Void in
if succeeded {
print("Woohoo, the user is linked with Facebook!")
}
})
}
The steps that happen when linking are very similar to log in. The difference is that on successful login, the existing PFUser
is updated with the Facebook information. Future logins via Facebook will now log in the user to their existing account.
If you want to unlink Facebook from a user, simply do this:
[PFFacebookUtils unlinkUserInBackground:user block:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"The user is no longer associated with their Facebook account.");
}
}];
PFFacebookUtils.unlinkUserInBackground(user, {
(succeeded: Bool?, error: NSError?) -> Void in
if succeeded {
print("The user is no longer associated with their Facebook account.")
}
})
In the previous sections, you’ve seen how PFFacebookUtils
can be used to log in with the Facebook SDK and create a PFUser
or link with existing ones. If you have already integrated the Facebook SDK and have a FBSDKAccessToken
, there is an option to directly log in or link the users like this:
FBSDKAccessToken *accessToken = ...; // Use existing access token.
// Log In (create/update currentUser) with FBSDKAccessToken
[PFFacebookUtils logInInBackgroundWithAccessToken:accessToken
block:^(PFUser *user, NSError *error) {
if (!user) {
NSLog(@"Uh oh. There was an error logging in.");
} else {
NSLog(@"User logged in through Facebook!");
}
}];
//
// or
//
// Link PFUser with FBSDKAccessToken
[PFFacebookUtils linkUserInBackground:user
withAccessToken:accessToken
block:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Woohoo, the user is linked with Facebook!");
}
}];
let accessToken: FBSDKAccessToken = ...; // Use existing access token.
// Log In (create/update currentUser) with FBSDKAccessToken
PFFacebookUtils.logInInBackgroundWithAccessToken(accessToken, {
(user: PFUser?, error: NSError?) -> Void in
if user != nil {
print("User logged in through Facebook!")
} else {
print("Uh oh. There was an error logging in.")
}
})
//
// or
//
// Link PFUser with FBSDKAccessToken
PFFacebookUtils.linkUserInBackground(user, withAccessToken: accessToken, {
(succeeded: Bool?, error: NSError?) -> Void in
if succeeded {
print("Woohoo, the user is linked with Facebook!")
}
})
Since Facebook SDK v4.0 - it is required to request read and publish permissions separately. With Parse SDK integration you can do that by logging in with read permissions first, and later, when the user wants to post to Facebook - linking a user with new set of publish permissions. This also works the other way around: logging in with publish permissions and linking with additional read permissions.
// Log In with Read Permissions
[PFFacebookUtils logInInBackgroundWithReadPermissions:permissions block:^(PFUser *user, NSError *error) {
if (!user) {
NSLog(@"Uh oh. The user cancelled the Facebook login.");
} else if (user.isNew) {
NSLog(@"User signed up and logged in through Facebook!");
} else {
NSLog(@"User logged in through Facebook!");
}
}];
// Request new Publish Permissions
[PFFacebookUtils linkUserInBackground:[PFUser currentUser]
withPublishPermissions:@[ @"publish_actions"]
block:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"User now has read and publish permissions!");
}
}];
// Log In with Read Permissions
PFFacebookUtils.logInInBackgroundWithReadPermissions(permissions, {
(user: PFUser?, error: NSError?) -> Void in
if let user = user {
if user.isNew {
print("User signed up and logged in through Facebook!")
} else {
print("User logged in through Facebook!")
}
} else {
print("Uh oh. The user cancelled the Facebook login.")
}
})
// Request new Publish Permissions
PFFacebookUtils.linkUserInBackground(user, withPublishPermissions: ["publish_actions"], {
(succeeded: Bool?, error: NSError?) -> Void in
if succeeded {
print("User now has read and publish permissions!")
}
})
The Facebook iOS SDK provides a number of helper classes for interacting with Facebook’s API. Generally, you will use the FBSDKGraphRequest
class to interact with Facebook on behalf of your logged-in user. You can read more about the Facebook SDK here.
To access the user’s Facebook access token, you can simply call [FBSDKAccessToken currentAccessToken]
to access the FBSDKAccessToken
instance, which can be passed to FBSDKGraphRequest
s.</p>
As with Facebook, Parse also provides an easy way to integrate Twitter authentication into your application. The Parse SDK provides a straightforward way to authorize and link a Twitter account to your PFUser
s. With just a few lines of code, you’ll be able to provide a “log in with Twitter” option in your app, and be able to save their data to Parse.
To start using Twitter with Parse, you need to:
http://twitter-oauth.callback
. This value will not be used by your iOS or Android application, but is necessary in order to enable authentication through Twitter. (See this issue)Accounts.framework
and Social.framework
libraries to your Xcode project.application:didFinishLaunchingWithOptions:
.[PFTwitterUtils initializeWithConsumerKey:@"YOUR CONSUMER KEY"
consumerSecret:@"YOUR CONSUMER SECRET"];
PFTwitterUtils.initializeWithConsumerKey("YOUR CONSUMER KEY", consumerSecret:"YOUR CONSUMER SECRET")
If you encounter any issues that are Twitter-related, a good resource is the official Twitter documentation.
There are two main ways to use Twitter with your Parse users: (1) logging in as a Twitter user and creating a PFUser
, or (2) linking Twitter to an existing PFUser
.
PFTwitterUtils
provides a way to allow your PFUser
s to log in or sign up through Twitter. This is accomplished using the logInWithBlock
or logInWithTarget
messages:
[PFTwitterUtils logInWithBlock:^(PFUser *user, NSError *error) {
if (!user) {
NSLog(@"Uh oh. The user cancelled the Twitter login.");
return;
} else if (user.isNew) {
NSLog(@"User signed up and logged in with Twitter!");
} else {
NSLog(@"User logged in with Twitter!");
}
}];
PFTwitterUtils.logInWithBlock {
(user: PFUser?, error: NSError?) -> Void in
if let user = user {
if user.isNew {
print("User signed up and logged in with Twitter!")
} else {
print("User logged in with Twitter!")
}
} else {
print("Uh oh. The user cancelled the Twitter login.")
}
}
When this code is run, the following happens:
PFUser
. If it’s a new user based on the Twitter handle, then that user is created.block
is called with the user.If you want to associate an existing PFUser
with a Twitter account, you can link it like so:
if (![PFTwitterUtils isLinkedWithUser:user]) {
[PFTwitterUtils linkUser:user block:^(BOOL succeeded, NSError *error) {
if ([PFTwitterUtils isLinkedWithUser:user]) {
NSLog(@"Woohoo, user logged in with Twitter!");
}
}];
}
if !PFTwitterUtils.isLinkedWithUser(user) {
PFTwitterUtils.linkUser(user, {
(succeeded: Bool?, error: NSError?) -> Void in
if PFTwitterUtils.isLinkedWithUser(user) {
print("Woohoo, user logged in with Twitter!")
}
})
}
The steps that happen when linking are very similar to log in. The difference is that on successful login, the existing PFUser
is updated with the Twitter information. Future logins via Twitter will now log the user into their existing account.
If you want to unlink Twitter from a user, simply do this:
[PFTwitterUtils unlinkUserInBackground:user block:^(BOOL succeeded, NSError *error) {
if (!error && succeeded) {
NSLog(@"The user is no longer associated with their Twitter account.");
}
}];
PFTwitterUtils.unlinkUserInBackground(user, {
(succeeded: Bool?, error: NSError?) -> Void in
if error == nil && succeeded {
print("The user is no longer associated with their Twitter account.")
}
})
Our SDK provides a straightforward way to sign your API HTTP requests to the Twitter REST API when your app has a Twitter-linked PFUser
. To make a request through our API, you can use the PF_Twitter
singleton provided by PFTwitterUtils
:
NSURL *verify = [NSURL URLWithString:@"https://api.twitter.com/1.1/account/verify_credentials.json"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:verify];
[[PFTwitterUtils twitter] signRequest:request];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// Check for error
// Data will contain the response data
}];
[task resume];
let verify = NSURL(string: "https://api.twitter.com/1.1/account/verify_credentials.json")
var request = NSMutableURLRequest(URL: verify!)
PFTwitterUtils.twitter()!.signRequest(request)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
// Check for error
// Data will contain the response data
}
task.resume()
Sessions represent an instance of a user logged into a device. Sessions are automatically created when users log in or sign up. They are automatically deleted when users log out. There is one distinct Session
object for each user-installation pair; if a user issues a login request from a device they’re already logged into, that user’s previous Session
object for that Installation is automatically deleted. Session
objects are stored on Parse in the Session class, and you can view them on the Parse Dashboard Data Browser. We provide a set of APIs to manage Session
objects in your app.
Session
is a subclass of a Parse Object
, so you can query, update, and delete sessions in the same way that you manipulate normal objects on Parse. Because Parse Server automatically creates sessions when you log in or sign up users, you should not manually create Session
objects unless you are building an IoT app (e.g. Arduino or Embedded C). Deleting a Session
will log the user out of the device that is currently using this session’s token.
Unlike other Parse objects, the Session
class does not have Cloud Code triggers. So you cannot register a beforeSave
or afterSave
handler for the Session class.
Session
PropertiesThe Session
object has these special fields:
sessionToken
(readonly): String token for authentication on Parse API requests. In the response of Session
queries, only your current Session
object will contain a session token.user
: (readonly) Pointer to the User
object that this session is for.createdWith
(readonly): Information about how this session was created (e.g. { "action": "login", "authProvider": "password"}
).
action
could have values: login
, signup
, create
, or upgrade
. The create
action is when the developer manually creates the session by saving a Session
object. The upgrade
action is when the user is upgraded to revocable session from a legacy session token.authProvider
could have values: password
, anonymous
, facebook
, or twitter
.expiresAt
(readonly): Approximate UTC date when this Session
object will be automatically deleted. You can configure session expiration settings (either 1-year inactivity expiration or no expiration) in your app’s Parse Dashboard settings page.installationId
(can be set only once): String referring to the Installation
where the session is logged in from. For Parse SDKs, this field will be automatically set when users log in or sign up.
All special fields except installationId
can only be set automatically by Parse Server. You can add custom fields onto Session
objects, but please keep in mind that any logged-in device (with session token) can read other sessions that belong to the same user (unless you disable Class-Level Permissions, see below).With revocable sessions, your current session token could become invalid if its corresponding Session
object is deleted from your Parse Server. This could happen if you implement a Session Manager UI that lets users log out of other devices, or if you manually delete the session via Cloud Code, REST API, or Data Browser. Sessions could also be deleted due to automatic expiration (if configured in app settings). When a device’s session token no longer corresponds to a Session
object on your Parse Server, all API requests from that device will fail with “Error 209: invalid session token”.
To handle this error, we recommend writing a global utility function that is called by all of your Parse request error callbacks. You can then handle the “invalid session token” error in this global function. You should prompt the user to login again so that they can obtain a new session token. This code could look like this:
// Objective-C
@interface ParseErrorHandlingController : NSObject
+ (void)handleParseError:(NSError *)error;
@end
@implementation ParseErrorHandlingController
+ (void)handleParseError:(NSError *)error {
if (![error.domain isEqualToString:PFParseErrorDomain]) {
return;
}
switch (error.code) {
case kPFErrorInvalidSessionToken: {
[self _handleInvalidSessionTokenError];
break;
}
... // Other Parse API Errors that you want to explicitly handle.
}
}
+ (void)_handleInvalidSessionTokenError {
//--------------------------------------
// Option 1: Show a message asking the user to log out and log back in.
//--------------------------------------
// If the user needs to finish what they were doing, they have the opportunity to do so.
//
// UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Invalid Session"
// message:@"Session is no longer valid, please log out and log in again."
// delegate:self
// cancelButtonTitle:@"Not Now"
// otherButtonTitles:@"OK"];
// [alertView show];
//--------------------------------------
// Option #2: Show login screen so user can re-authenticate.
//--------------------------------------
// You may want this if the logout button is inaccessible in the UI.
//
// UIViewController *presentingViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
// PFLogInViewController *logInViewController = [[PFLogInViewController alloc] init];
// [presentingViewController presentViewController:logInViewController animated:YES completion:nil];
}
@end
// In all API requests, call the global error handler, e.g.
[[PFQuery queryWithClassName:@"Object"] findInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// Query succeeded - continue your app logic here.
} else {
// Query failed - handle an error.
[ParseErrorHandlingController handleParseError:error];
}
}];
// Swift
class ParseErrorHandlingController {
class func handleParseError(error: NSError) {
if error.domain != PFParseErrorDomain {
return
}
switch (error.code) {
case kPFErrorInvalidSessionToken:
handleInvalidSessionTokenError()
... // Other Parse API Errors that you want to explicitly handle.
}
private class func handleInvalidSessionTokenError() {
//--------------------------------------
// Option 1: Show a message asking the user to log out and log back in.
//--------------------------------------
// If the user needs to finish what they were doing, they have the opportunity to do so.
//
// let alertView = UIAlertView(
// title: "Invalid Session",
// message: "Session is no longer valid, please log out and log in again.",
// delegate: nil,
// cancelButtonTitle: "Not Now",
// otherButtonTitles: "OK"
// )
// alertView.show()
//--------------------------------------
// Option #2: Show login screen so user can re-authenticate.
//--------------------------------------
// You may want this if the logout button is inaccessible in the UI.
//
// let presentingViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
// let logInViewController = PFLogInViewController()
// presentingViewController?.presentViewController(logInViewController, animated: true, completion: nil)
}
}
// In all API requests, call the global error handler, e.g.
let query = PFQuery(className: "Object")
query.findObjectsInBackgroundWithBlock { (objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
// Query Succeeded - continue your app logic here.
} else {
// Query Failed - handle an error.
ParseErrorHandlingController.handleParseError(error)
}
}
Session
SecuritySession
objects can only be accessed by the user specified in the user field. All Session
objects have an ACL that is read and write by that user only. You cannot change this ACL. This means querying for sessions will only return objects that match the current logged-in user.
When you log in a user via a User
login method, Parse will automatically create a new unrestricted Session
object in your Parse Server. Same for signups and Facebook/Twitter logins.
You can configure Class-Level Permissions (CLPs) for the Session class just like other classes on Parse. CLPs restrict reading/writing of sessions via the Session
API, but do not restrict Parse Server’s automatic session creation/deletion when users log in, sign up, and log out. We recommend that you disable all CLPs not needed by your app. Here are some common use cases for Session CLPs:
As your app grows in scope and user-base, you may find yourself needing more coarse-grained control over access to pieces of your data than user-linked ACLs can provide. To address this requirement, Parse supports a form of Role-based Access Control. Roles provide a logical way of grouping users with common access privileges to your Parse data. Roles are named objects that contain users and other roles. Any permission granted to a role is implicitly granted to its users as well as to the users of any roles that it contains.
For example, in your application with curated content, you may have a number of users that are considered “Moderators” and can modify and delete content created by other users. You may also have a set of users that are “Administrators” and are allowed all of the same privileges as Moderators, but can also modify the global settings for the application. By adding users to these roles, you can ensure that new users can be made moderators or administrators, without having to manually grant permission to every resource for each user.
We provide a specialized class called PFRole
that represents these role objects in your client code. PFRole
is a subclass of PFObject
, and has all of the same features, such as a flexible schema, automatic persistence, and a key value interface. All the methods that are on PFObject
also exist on PFRole
. The difference is that PFRole
has some additions specific to management of roles.
PFRole
PropertiesPFRole
has several properties that set it apart from PFObject
:
The PFRole
uses the same security scheme (ACLs) as all other objects on Parse, except that it requires an ACL to be set explicitly. Generally, only users with greatly elevated privileges (e.g. a master user or Administrator) should be able to create or modify a Role, so you should define its ACLs accordingly. Remember, if you give write-access to a PFRole
to a user, that user can add other users to the role, or even delete the role altogether.
To create a new PFRole
, you would write:
// By specifying no write privileges for the ACL, we can ensure the role cannot be altered.
PFACL *roleACL = [PFACL ACL];
[roleACL setPublicReadAccess:YES];
PFRole *role = [PFRole roleWithName:@"Administrator" acl:roleACL];
[role saveInBackground];
// By specifying no write privileges for the ACL, we can ensure the role cannot be altered.
var roleACL = PFACL()
roleACL.setPublicReadAccess(true)
var role = PFRole.roleWithName("Administrator", acl:roleACL)
role.saveInBackground()
You can add users and roles that should inherit your new role’s permissions through the “users” and “roles” relations on PFRole
:
PFRole *role = [PFRole roleWithName:roleName acl:roleACL];
for (PFUser *user in usersToAddToRole) {
[role.users addObject:user];
}
for (PFRole *childRole in rolesToAddToRole) {
[role.roles addObject:childRole];
}
[role saveInBackground];
var role = PFRole.roleWithName(roleName, acl:roleACL)
for user in usersToAddToRole {
role.users.addObject(user)
}
for childRole in rolesToAddToRole {
role.roles.addObject(childRole)
}
role.saveInBackground()
Take great care when assigning ACLs to your roles so that they can only be modified by those who should have permissions to modify them.
Now that you have created a set of roles for use in your application, you can use them with ACLs to define the privileges that their users will receive. Each PFObject
can specify a PFACL
, which provides an access control list that indicates which users and roles should be granted read or write access to the object.
Giving a role read or write permission to an object is straightforward. You can either use the PFRole
:
PFRole *moderators = /* Query for some PFRole */;
PFObject *wallPost = [PFObject objectWithClassName:@"WallPost"];
PFACL *postACL = [PFACL ACL];
[postACL setWriteAccess:YES forRole:moderators];
wallPost.ACL = postACL;
[wallPost saveInBackground];
var moderators = /* Query for some PFRole */
var wallPost = PFObject(className: "WallPost")
var postACL = PFACL()
postACL.setWriteAccess(true, forRole:moderators)
wallPost.ACL = postACL
wallPost.saveInBackground()
You can avoid querying for a role by specifying its name for the ACL:
PFObject *wallPost = [PFObject objectWithClassName:@"WallPost"];
PFACL *postACL = [PFACL ACL];
[postACL setWriteAccess:YES forRoleWithName:@"Moderators"];
wallPost.ACL = postACL;
[wallPost saveInBackground];
var wallPost = PFObject(className: "WallPost")
var postACL = PFACL()
postACL.setWriteAccess(true, forRoleWithName: "Moderators")
wallPost.ACL = postACL
wallPost.saveInBackground()
Role-based PFACL
s can also be used when specifying default ACLs for your application, making it easy to protect your users’ data while granting access to users with additional privileges. For example, a moderated forum application might specify a default ACL like this:
PFACL *defaultACL = [PFACL ACL];
// Everybody can read objects created by this user
[defaultACL setPublicReadAccess:YES];
// Moderators can also modify these objects
[defaultACL setWriteAccess:YES forRoleWithName:@"Moderators"];
// And the user can read and modify its own objects
[PFACL setDefaultACL:defaultACL withAccessForCurrentUser:YES];
var defaultACL = PFACL()
// Everybody can read objects created by this user
defaultACL.hasPublicWriteAccess = true
// Moderators can also modify these objects
defaultACL.setWriteAccess(true, forRoleWithName: "Moderators")
// And the user can read and modify its own objects
PFACL.setDefault(defaultACL, withAccessForCurrentUser:true)
As described above, one role can contain another, establishing a parent-child relationship between the two roles. The consequence of this relationship is that any permission granted to the parent role is implicitly granted to all of its child roles.
These types of relationships are commonly found in applications with user-managed content, such as forums. Some small subset of users are “Administrators”, with the highest level of access to tweaking the application’s settings, creating new forums, setting global messages, and so on. Another set of users are “Moderators”, who are responsible for ensuring that the content created by users remains appropriate. Any user with Administrator privileges should also be granted the permissions of any Moderator. To establish this relationship, you would make your “Administrators” role a child role of “Moderators”, like this:
PFRole *administrators = /* Your "Administrators" role */;
PFRole *moderators = /* Your "Moderators" role */;
[moderators.roles addObject:administrators];
[moderators saveInBackground];
var administrators = /* Your "Administrators" role */
var moderators = /* Your "Moderators" role */
moderators.roles.addObject(administrators)
moderators.saveInBackground()
PFFileObject
lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular PFObject
. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data.
Getting started with PFFileObject
is easy. First, you’ll need to have the data in NSData
form and then create a PFFileObject
with it. In this example, we’ll just use a string:
NSData *data = [@"Working at Parse is great!" dataUsingEncoding:NSUTF8StringEncoding];
PFFileObject *file = [PFFileObject fileObjectWithName:@"resume.txt" data:data];
let str = "Working at Parse is great!"
let data = str.data(using: String.Encoding.utf8)
let file = PFFileObject(name:"resume.txt", data:data!)
Notice in this example that we give the file a name of resume.txt
. There’s two things to note here:
resume.txt
..png
.Next you’ll want to save the file up to the cloud. As with PFObject
, there are many variants of the save
method you can use depending on what sort of callback and error handling suits you.
[file saveInBackground];
file?.saveInBackground()
Finally, after the save completes, you can associate a PFFileObject
onto a PFObject
just like any other piece of data:
PFObject *jobApplication = [PFObject objectWithClassName:@"JobApplication"]
jobApplication[@"applicantName"] = @"Joe Smith";
jobApplication[@"applicantResumeFile"] = file;
[jobApplication saveInBackground];
let jobApplication = PFObject(className:"JobApplication")
jobApplication["applicantName"] = "Joe Smith"
jobApplication["applicantResumeFile"] = file
jobApplication.saveInBackground()
Retrieving it back involves calling one of the getData
variants on the PFFileObject
. Here we retrieve the resume file off another JobApplication object:
PFFileObject *applicantResume = anotherApplication[@"applicantResumeFile"];
NSData *resumeData = [applicantResume getData];
let applicantResume = annotherApplication["applicationResumeFile"] as PFFileObject
let resumeData = applicantResume.getData()
Just like on PFObject
, you will most likely want to use the background version of getData
.
You can easily store images by converting them to NSData
and then using PFFileObject
. Suppose you have a UIImage
named image
that you want to save as a PFFileObject
:
NSData *imageData = UIImagePNGRepresentation(image);
PFFileObject *imageFile = [PFFileObject fileObjectWithName:@"image.png" data:imageData];
PFObject *userPhoto = [PFObject objectWithClassName:@"UserPhoto"];
userPhoto[@"imageName"] = @"My trip to Hawaii!";
userPhoto[@"imageFile"] = imageFile;
[userPhoto saveInBackground];
let imageData = UIImagePNGRepresentation(image)
let imageFile = PFFileObject(name:"image.png", data:imageData)
var userPhoto = PFObject(className:"UserPhoto")
userPhoto["imageName"] = "My trip to Hawaii!"
userPhoto["imageFile"] = imageFile
userPhoto.saveInBackground()
Your PFFileObject
will be uploaded as part of the save operation on the userPhoto
object. It’s also possible to track a PFFileObject
’s upload and download progress.
Retrieving the image back involves calling one of the getData
variants on the PFFileObject
. Here we retrieve the image file off another UserPhoto
named anotherPhoto
:
PFFileObject *userImageFile = anotherPhoto[@"imageFile"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *image = [UIImage imageWithData:imageData];
}
}];
let userImageFile = anotherPhoto["imageFile"] as! PFFileObject
userImageFile.getDataInBackground { (imageData: Data?, error: Error?) in
if let error = error {
print(error.localizedDescription)
} else if let imageData = imageData {
let image = UIImage(data:imageData)
}
}
It’s easy to get the progress of both uploads and downloads using PFFileObject
using saveInBackgroundWithBlock:progressBlock:
and getDataInBackgroundWithBlock:progressBlock:
respectively. For example:
NSData *data = [@"Working at Parse is great!" dataUsingEncoding:NSUTF8StringEncoding];
PFFileObject *file = [PFFileObject fileObjectWithName:@"resume.txt" data:data];
[file saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
// Handle success or failure here ...
} progressBlock:^(int percentDone) {
// Update your progress spinner here. percentDone will be between 0 and 100.
}];
let str = "Working at Parse is great!"
let data = str.data(using: String.Encoding.utf8)
let file = PFFileObject(name:"resume.txt", data:data!)
file?.saveInBackground({ (success: Bool, error: Error?) in
// Handle success or failure here ...
}, progressBlock: { (percentDone: Int32) in
// Update your progress spinner here. percentDone will be between 0 and 100.
})
If you know the name of a file you can delete it using the REST API. Your master key is required for this operation.
Note: Reguardless of the Parse Server storage configuration, deleting a PFObject
with a PFFileObject
does not delete the file itself meerly its reference. Additionally, Parse does NOT provide a way to find unreferenced file names in storage.
Parse allows you to associate real-world latitude and longitude coordinates with an object. Adding a PFGeoPoint
to a PFObject
allows queries to take into account the proximity of an object to a reference point. This allows you to easily do things like find out what user is closest to another user or which places are closest to a user.
To associate a point with an object you first need to create a PFGeoPoint
. For example, to create a point with latitude of 40.0 degrees and -30.0 degrees longitude:
PFGeoPoint *point = [PFGeoPoint geoPointWithLatitude:40.0 longitude:-30.0];
let point = PFGeoPoint(latitude:40.0, longitude:-30.0)
This point is then stored in the object as a regular field.
placeObject[@"location"] = point;
placeObject["location"] = point
Note: Currently only one key in a class may be a PFGeoPoint
.
PFGeoPoint
also provides a helper method for fetching the user’s current location. This is accomplished via geoPointForCurrentLocationInBackground
:
[PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) {
if (!error) {
// do something with the new geoPoint
}
}];
PFGeoPoint.geoPointForCurrentLocationInBackground {
(geoPoint: PFGeoPoint?, error: NSError?) -> Void in
if error == nil {
// do something with the new geoPoint
}
}
When this code is run, the following happens:
CLLocationManager
starts listening for location updates (via startsUpdatingLocation
).stopsUpdatingLocation
) and a PFGeoPoint
is created from the new location. If the location manager errors out, it still stops listening for updates, and returns an NSError
instead.block
is called with the PFGeoPoint
.For those who choose to use CLLocationManager
directly, we also provide a +geoPointWithLocation:
constructor to transform CLLocation
s directly into PFGeoPoint
s - great for apps that require constant polling.
Parse allows you to associate polygon coordinates with an object. Adding a PFPolygon
to a PFObject
allows queries to determine whether a PFGeoPoint
is within a PFPolygon
or if a PFPolygon
contains a PFGeoPoint
.
For example, to create a polygon with coordinates (0, 0), (0, 1), (1, 1), (1, 0).
NSArray *points = @[@[@0,@0],@[@0,@1],@[@1,@1],@[@1,@0]];
PFPolygon *polygon = [PFPolygon polygonWithCoordinates:points];
let points = [[0,0], [0,1], [1,1], [1,0]]
let polygon = PFPolygon(coordinates: points)
This polygon is then stored in the object as a regular field.
placeObject[@"bounds"] = polygon;
placeObject["bounds"] = polygon
Now that you have a bunch of objects with spatial coordinates, it would be nice to find out which objects are closest to a point. This can be done by adding another restriction to PFQuery
using whereKey:nearGeoPoint:
. Getting a list of ten places that are closest to a user may look something like:
// User's location
PFGeoPoint *userGeoPoint = userObject[@"location"];
// Create a query for places
PFQuery *query = [PFQuery queryWithClassName:@"PlaceObject"];
// Interested in locations near user.
[query whereKey:@"location" nearGeoPoint:userGeoPoint];
// Limit what could be a lot of points.
query.limit = 10;
// Final list of objects
placesObjects = [query findObjects];
// User's location
let userGeoPoint = userObject["location"] as PFGeoPoint
// Create a query for places
var query = PFQuery(className:"PlaceObject")
// Interested in locations near user.
query.whereKey("location", nearGeoPoint:userGeoPoint)
// Limit what could be a lot of points.
query.limit = 10
// Final list of objects
placesObjects = query.findObjects()
At this point placesObjects
will be an array of objects ordered by distance (nearest to farthest) from userGeoPoint
. Note that if an additional orderByAscending:
/orderByDescending:
constraint is applied, it will take precedence over the distance ordering.
To limit the results using distance check out whereKey:nearGeoPoint:withinMiles
, whereKey:nearGeoPoint:withinKilometers
, and whereKey:nearGeoPoint:withinRadians
.
It’s also possible to query for the set of objects that are contained within a particular area. To find the objects in a rectangular bounding box, add the whereKey:withinGeoBoxFromSouthwest:toNortheast:
restriction to your PFQuery
.
PFGeoPoint *swOfSF = [PFGeoPoint geoPointWithLatitude:37.708813 longitude:-122.526398];
PFGeoPoint *neOfSF = [PFGeoPoint geoPointWithLatitude:37.822802 longitude:-122.373962];
PFQuery *query = [PFQuery queryWithClassName:@"PizzaPlaceObject"];
[query whereKey:@"location" withinGeoBoxFromSouthwest:swOfSF toNortheast:neOfSF];
NSArray *pizzaPlacesInSF = [query findObjects];
let swOfSF = PFGeoPoint(latitude:37.708813, longitude:-122.526398)
let neOfSF = PFGeoPoint(latitude:37.822802, longitude:-122.373962)
var query = PFQuery(className:"PizzaPlaceObject")
query.whereKey("location", withinGeoBoxFromSouthwest:swOfSF, toNortheast:neOfSF)
var pizzaPlacesInSF = query.findObjects()
You can query for whether an object’s PFGeoPoint
lies within or on a polygon formed of Parse.GeoPoint
:
PFGeoPoint *geoPoint1 = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0];
PFGeoPoint *geoPoint2 = [PFGeoPoint geoPointWithLatitude:20.0 longitude:30.0];
PFGeoPoint *geoPoint3 = [PFGeoPoint geoPointWithLatitude:30.0 longitude:40.0];
PFQuery *query = [PFQuery queryWithClassName:@"Locations"];
[query whereKey:@"location" withinPolygon:@[geoPoint1, geoPoint2, geoPoint3]];
let geoPoint1 = PFGeoPoint(latitude: 10.0, longitude: 20.0)
let geoPoint2 = PFGeoPoint(latitude: 20.0, longitude: 30.0)
let geoPoint3 = PFGeoPoint(latitude: 30.0, longitude: 40.0)
let query = PFQuery(className: "Locations")
query.whereKey("location", withinPolygon: [geoPoint1, geoPoint2, geoPoint3])
You can also query for whether an object Parse.Polygon
contains a Parse.GeoPoint
:
PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:0.5 longitude:0.5];
PFQuery *query = [PFQuery queryWithClassName:@"Locations"];
[query whereKey:@"bounds" polygonContains:geoPoint];
let geoPoint = PFGeoPoint(latitude: 0.5, longitude: 0.5)
let query = PFQuery(className: "Locations")
query.whereKey("bounds", polygonContains: geoPoint)
To efficiently find if a PFPolygon
contains a PFGeoPoint
without querying use containsPoint
.
NSArray *points = @[@[@0,@0],@[@0,@1],@[@1,@1],@[@1,@0]];
PFPolygon *polygon = [PFPolygon polygonWithCoordinates:points];
PFGeoPoint *inside = [PFGeoPoint geoPointWithLatitude:0.5 longitude:0.5];
PFGeoPoint *outside = [PFGeoPoint geoPointWithLatitude:10 longitude:10];
// Returns True
[polygon containsPoint:inside];
// Returns False
[polygon containsPoint:outside];
let points = [[0,0], [0,1], [1,1], [1,0]]
let polygon = PFPolygon(coordinates: points)
let inside = PFGeoPoint(latitude: 0.5, longitude: 0.5)
let outside = PFGeoPoint(latitude: 10, longitude: 10)
// Returns true
polygon.contains(inside)
// Returns false
polygon.contains(outside)
At the moment there are a couple of things to watch out for:
PFObject
class may only have one key with a PFGeoPoint
object.nearGeoPoint
constraint will also limit results to within 100 miles.The Parse iOS/OS X SDK provides a local datastore which can be used to store and retrieve PFObject
s, even when the network is unavailable. To enable this functionality add isLocalDatastoreEnabled = true
to the ParseClientConfiguration
block used in Parse.initialize()
or call Parse.enableLocalDatastore()
prior to initializing Parse.
@implementation AppDelegate
- (void)application:(UIApplication *)application didFinishLaunchWithOptions:(NSDictionary *)options {
ParseClientConfiguration *configuration = [ParseClientConfiguration configurationWithBlock:^(id<ParseMutableClientConfiguration> configuration) {
configuration.applicationId = @"parseAppId";
configuration.clientKey = @"parseClientKey";
configuration.server = @"parseServerUrlString";
configuration.localDatastoreEnabled = YES;
}];
[Parse initializeWithConfiguration:configuration];
}
@end
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let parseConfig = ParseClientConfiguration {
$0.isLocalDatastoreEnabled = true
$0.applicationId = parseApplicationId
$0.clientKey = parseClientKey
$0.server = parseServerUrlString
}
Parse.initialize(with: parseConfig)
}
}
There are a couple of side effects of enabling the local datastore that you should be aware of. When enabled, there will only be one instance of any given PFObject
. For example, imagine you have an instance of the "GameScore"
class with an objectId
of "xWMyZ4YEGZ"
, and then you issue a PFQuery
for all instances of "GameScore"
with that objectId
. The result will be the same instance of the object you already have in memory.
Another side effect is that the current user and current installation will be stored in the local datastore, so you can persist unsaved changes to these objects between runs of your app using the methods below.
Calling the saveEventually
method on a PFObject
will cause the object to be pinned in the local datastore until the save completes. So now, if you change the current PFUser
and call [[PFUser currentUser] saveEventually]
, your app will always see the changes that you have made.
You can store a PFObject
in the local datastore by pinning it. Pinning a PFObject
is recursive, just like saving, so any objects that are pointed to by the one you are pinning will also be pinned. When an object is pinned, every time you update it by fetching or saving new data, the copy in the local datastore will be updated automatically. You don’t need to worry about it at all.
PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;
[gameScore pinInBackground];
let gameScore = PFObject(className:"GameScore")
gameScore["score"] = 1337
gameScore["playerName"] = "Sean Plott"
gameScore["cheatMode"] = false
gameScore.pinInBackground()
If you have multiple objects, you can pin them all at once with the pinAllInBackground
convenience method.
[PFObject pinAllInBackground:listOfObjects];
PFObject.pinAllInBackground(listOfObjects)
Storing objects is great, but it’s only useful if you can then get the objects back out later. Retrieving an object from the local datastore works just like retrieving one over the network. The only difference is calling the fromLocalDatastore
method to tell the PFQuery
where to look for its results.
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query fromLocalDatastore];
[query getObjectInBackgroundWithId:"" block:^(PFObject * _Nullable object, NSError * _Nullable error) {
if (!error) {
// Success
} else {
// Fail!
}
}
// task.result will be your game score
return task;
}];
let query = PFQuery(className: "GameScore")
query.fromLocalDatastore()
query.getObjectInBackground(withId: "string") { (object, error) in
if error == nil {
// Success!
} else {
// Failure!
}
}
Often, you’ll want to find a whole list of objects that match certain criteria, instead of getting a single object by id. To do that, you can use a PFQuery. Any PFQuery
can be used with the local datastore just as with the network. The results will include any object you have pinned that matches the query. Any unsaved changes you have made to the object will be considered when evaluating the query. So you can find a local object that matches, even if it was never returned from the server for this particular query.
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query fromLocalDatastore];
[query whereKey:@"playerName" equalTo:@"Joe Bob"];
[[query findObjectsInBackground] continueWithBlock:^id(BFTask *task) {
if (task.error) {
NSLog(@"Error: %@", task.error);
return task;
}
NSLog(@"Retrieved %d", task.result.count);
return task;
}];
let query = PFQuery(className: "GameScore")
query.fromLocalDatastore()
query.whereKey("playerName", equalTo: "Joe Bob")
query.findObjectsInBackground().continueWithBlock {
(task: BFTask!) -> AnyObject in
if let error = task.error {
print("Error: \(error)")
return task
}
print("Retrieved \(task.result.count)")
return task
}
The same security model that applies to objects in Parse applies to objects in the Local Datastore. Read-write permissions are defined by PFACL
s and a user cannot access or modify anything they don’t have permission to.
The only difference is that you won’t be able to access any data protected by Role based ACLs due to the fact that the Roles are stored on the server. To access this data protected by Role based ACLs, you will need to ignore ACLs when executing a Local Datastore query:
PFQuery *query = [[[PFQuery queryWithClassName:@"Note"]
fromLocalDatastore]
ignoreACLs];
let query = PFQuery(className: "Note")
.fromLocalDatastore
.ignoreACLs
When you are done with an object and no longer need it to be in the local datastore, you can simply unpin it. This will free up disk space on the device and keep your queries on the local datastore running quickly.
[gameScore unpinInBackground];
gameScore.unpinInBackground()
There’s also a method to unpin several objects at once.
[PFObject unpinAllInBackground:listOfObjects];
PFObject.unpinAllInBackground(listOfObjects)
Manually pinning and unpinning each object individual is a bit like using malloc
and free
. It is a very powerful tool, but it can be difficult to manage what objects get stored in complex scenarios. For example, imagine you are making a game with separate high score lists for global high scores and your friends’ high scores. If one of your friends happens to have a globally high score, you need to make sure you don’t unpin them completely when you remove them from one of the cached queries. To make these scenarios easier, you can also pin with a label. Labels indicate a group of objects that should be stored together.
// Add several objects with a label.
[PFObject pinAllInBackground:someGameScores withName:@"MyScores"];
// Add another object with the same label.
[anotherGameScore pinInBackgroundWithName:@"MyScores"];
// Add several objects with a label.
PFObject.pinAllInBackground(objects:someGameScores withName:"MyScores")
// Add another object with the same label.
anotherGameScore.pinInBackgroundWithName("MyScores")
To unpin all of the objects with the same label at the same time, you can pass a label to the unpin methods. This saves you from having to manually track which objects are in each group you care about.
[PFObject unpinAllObjectsInBackgroundWithName:@"MyScores"];
PFObject.unpinAllObjectsInBackgroundWithName("MyScores")
Any object will stay in the datastore as long as it is pinned with any label. In other words, if you pin an object with two different labels, and then unpin it with one label, the object will stay in the datastore until you also unpin it with the other label.
Pinning with labels makes it easy to cache the results of queries. You can use one label to pin the results of each different query. To get new results from the network, just do a query and update the pinned objects.
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query orderByDescending:@"score"];
// Query for new results from the network
[[query findObjectsInBackground] continueWithSuccessBlock:^id(BFTask *task) {
return [[PFObject unpinAllObjectsInBackgroundWithName:@"HighScores"] continueWithSuccessBlock:^id(BFTask *ignored) {
// Cache the new results.
NSArray *scores = task.result;
return [PFObject pinAllInBackground:scores withName:@"HighScores"];
}];
}];
let query = PFQuery(className:"GameScore")
query.orderByDescending("score")
// Query for new results from the network
query.findObjectsInBackground().continueWithSuccessBlock({
(task: BFTask!) -> AnyObject! in
return PFObject.unpinAllObjectsInBackgroundWithName("HighScores").continueWithSuccessBlock({
(ignored: BFTask!) -> AnyObject! in
// Cache new results
let scores = task.result as? NSArray
return PFObject.pinAllInBackground(scores as [AnyObject], withName: "HighScores")
})
})
When you want to get the cached results for the query, you can then run the same query against the local datastore.
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query fromLocalDatastore];
[query orderByDescending:@"score"];
[[query findObjectsInBackground] continueWithBlock:^id(BFTask *task) {
if (task.error) {
// Something went wrong.
return task;
}
// Yay! Cached scores!
return task;
}];
let query = PFQuery(className:"GameScore")
query.fromLocalDatastore()
query.orderByDescending("score")
query.findObjectsInBackground().continueWithBlock({
(task: BFTask!) -> AnyObject! in
if task.error != nil {
// There was an error.
return task
}
// Yay! Cached scores!
return task
})
Once you’ve saved some changes locally, there are a few different ways you can save those changes back to Parse over the network. The easiest way to do this is with saveEventually
. When you call saveEventually
on a PFObject
, it will be pinned until it can be saved. The SDK will make sure to save the object the next time the network is available.
[gameScore saveEventually];
gameScore.saveEventually()
If you’d like to have more control over the way objects are synced, you can keep them in the local datastore until you are ready to save them yourself using saveInBackground
. To manage the set of objects that need to be saved, you can again use a label. The fromPinWithName:
method on PFQuery
makes it easy to fetch just the objects you care about.
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query fromPinWithName:@"MyChanges"];
[[query findObjectsInBackground] continueWithBlock:^id(BFTask *task) {
NSArray *scores = task.result;
for (PFObject *score in scores) {
[[score saveInBackground] continueWithSuccessBlock:^id(BFTask *task) {
return [score unpinInBackground];
}];
}
return task;
}];
let query = PFQuery(className:"GameScore")
query.fromPinWithName("MyChanges")
query.findObjectsInBackground().continueWithBlock({
(task: BFTask!) -> AnyObject! in
let scores = task.result as NSArray
for score in scores {
score.saveInBackground().continueWithSuccessBlock({
(task: BFTask!) -> AnyObject! in
return score.unpinInBackground()
})
}
return task
})
</div>
Local data sharing in Parse SDKs allows you do share persistent local data between your main application and extensions that it contains, including Keyboard, Share/Today/Photo/Action extensions and Document Providers.
All local data that is persistent can be shared between apps and multiple extensions:
PFUser currentUser
currentUser
PFInstallation currentInstallation
installationId
which is used for Analytics eventssaveEventually
and deleteEventually
pending operationsWe do not recommend storing large pieces of data inside these persistent objects, both with and without data sharing enabled, as it might impact the performance of how fast this data is saved and loaded.
Furthermore, if you are using Local Datastore with data sharing - we recommend that you divide your objects amongst multiple pins, as querying and persisting data in a smaller pin is usually faster.
To share your local data between app and extensions you need to do the following:
// Enable data sharing in main app.
[Parse enableDataSharingWithApplicationGroupIdentifier:@"group.com.parse.parseuidemo"];
// Setup Parse
[Parse setApplicationId:@"<ParseAppId>" clientKey:@"<ClientKey>"];
// Enable data sharing in main app.
Parse.enableDataSharingWithApplicationGroupIdentifier("group.com.parse.parseuidemo")
// Setup Parse
Parse.setApplicationId("<ParseAppId>", clientKey: "<ClientKey>")
// Enable data sharing in app extensions.
[Parse enableDataSharingWithApplicationGroupIdentifier:@"group.com.parse.parseuidemo"
containingApplication:@"com.parse.parseuidemo"];
// Setup Parse
[Parse setApplicationId:@"<ParseAppId>" clientKey:@"<ClientKey>"];
// Enable data sharing in app extensions.
Parse.enableDataSharingWithApplicationGroupIdentifier("group.com.parse.parseuidemo",
containingApplicaiton: "com.parse.parseuidemo")
// Setup Parse
Parse.setApplicationId("<ParseAppId>", clientKey: "<ClientKey>")
As you might have noticed - there are few pieces of information that need to be in sync for this to work and be enabled:
This process is required to be completed for both your main application and any extension.
After all these steps are done, you are all set and all data will be shared between your app and any app extension that the app contains.
If you have an existing app using the Parse SDK, when users upgrade to your latest app version (with data sharing enabled), the Parse SDK will automatically move the main app’s local persistent data (Local Datastore, current PFUser
, current PFInstallation
, etc) from the app’s sandbox container to the shared container.
This migration is irreversible; if you later release another app version with data sharing disabled,
we will not move the shared container’s data back into the main app’s sandbox container.
For more about the app/extension shared container, please see Apple’s documentation.
Push Notifications are a great way to keep your users engaged and informed about your app. You can reach your entire user base quickly and effectively. This guide will help you through the setup process and the general usage of Parse to send push notifications.
If you haven’t installed the SDK yet, please head over to the Push QuickStart to get our SDK up and running.
Please note that client push is not available with Parse Server due to it being a significant security risk, it is recommended that to trigger push notifications from your iOS app you run a cloud function that sends the push using the masterKey
. If you must use client push, you could fork Parse Server and enable it or alternatively Back4App offer it as an option for testing purposes only.
If you want to start using push, start by completing the Push Notifications QuickStart to learn how to configure your app. Come back to this guide afterwards to learn more about the push features offered by Parse.
Every Parse application installed on a device registered for push notifications has an associated PFInstallation object. The PFInstallation object is where you store all the data needed to target push notifications. For example, in a baseball app, you could store the teams a user is interested in to send updates about their performance. Saving the PFInstallation object is also required for tracking push-related app open events.
In iOS or OS X, Installation
objects are available through the PFInstallation
class, a subclass of PFObject
. It uses the same API for storing and retrieving data. To access the current Installation
object from your app, use the [PFInstallation currentInstallation]
method. The first time you save a PFInstallation
, Parse will add it to your Installation
class, and it will be available for targeting push notifications as long as its deviceToken
field is set.
First, make your app register for remote notifications by adding the following in your application:didFinishLaunchingWithOptions:
method (if you haven’t already):
UIUserNotificationType userNotificationTypes = (UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound);
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:nil];
[application registerUserNotificationSettings:settings];
[application registerForRemoteNotifications];
let userNotificationTypes: UIUserNotificationType = [.Alert, .Badge, .Sound]
let settings = UIUserNotificationSettings(forTypes: userNotificationTypes, categories: nil)
application.registerUserNotificationSettings(settings)
application.registerForRemoteNotifications()
We will then update our PFInstallation
with the deviceToken
once the device is registered for push notifications:
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
// Store the deviceToken in the current Installation and save it to Parse
PFInstallation *currentInstallation = [PFInstallation currentInstallation];
[currentInstallation setDeviceTokenFromData:deviceToken];
[currentInstallation saveInBackground];
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
// Store the deviceToken in the current Installation and save it to Parse
let installation = PFInstallation.currentInstallation()
installation.setDeviceTokenFromData(deviceToken)
installation.saveInBackground()
}
While it is possible to modify a PFInstallation
just like you would a PFObject
, there are several special fields that help manage and target devices.
channels
: An array of the channels to which a device is currently subscribed.badge
: The current value of the icon badge for iOS/OS X apps. Changing this value on the PFInstallation
will update the badge value on the app icon. Changes should be saved to the server so that they will be used for future badge-increment push notifications.installationId
: Unique Id for the device used by Parse (readonly).deviceType
: The type of device, “ios”, “tvos”, “osx”, “android”, “winrt”, “winphone”, “dotnet”, or “embedded”. On iOS, tvOS and OS X devices, this field will be set to “ios”, “tvos” and “osx”, respectively (readonly).deviceToken
: The Apple generated token used for iOS/OS X devices. On Android devices, this is the token used by FCM to keep track of registration ID (readonly).appName
: The display name of the client application to which this installation belongs. In iOS/OS X, this value is obtained from kCFBundleNameKey
. This value is synchronized every time a PFInstallation
object is saved from the device (readonly).appVersion
: The version string of the client application to which this installation belongs. In iOS/OS X, this value is obtained from kCFBundleVersionKey
. This value is synchronized every time a PFInstallation
object is saved from the device (readonly).appIdentifier
: A unique identifier for this installation’s client application. In iOS/OS X, this value is obtained from kCFBundleIdentifierKey
. This value is synchronized every time a PFInstallation
object is saved from the device (readonly).parseVersion
: The version of the Parse SDK which this installation uses. This value is synchronized every time a PFInstallation
object is saved from the device (readonly).timeZone
: The current time zone where the target device is located. This value is synchronized every time a PFInstallation
object is saved from the device (readonly).localeIdentifier
: The locale identifier of the device in the format [language code]-[COUNTRY CODE]. The language codes are two-letter lowercase ISO language codes (such as “en”) as defined by ISO 639-1. The country codes are two-letter uppercase ISO country codes (such as “US”) as defined by ISO 3166-1. This value is synchronized every time a PFInstallation
object is saved from the device (readonly).pushType
: This field is reserved for directing Parse to the push delivery network to be used for Android devices. This parameter is not supported in iOS/OS X devices (readonly).channelUris
: The Microsoft-generated push URIs for Windows devices (readonly).The Parse SDK will avoid making unnecessary requests. If a PFInstallation
is saved on the device, a request to the Parse servers will only be made if one of the PFInstallation
’s fields has been explicitly updated.
There are two ways to send push notifications using Parse: channels and advanced targeting. Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section.
Sending notifications is often done from the Parse Dashboard push console, the REST API or from Cloud Code.
You can view your past push notifications on the Parse Dashboard push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs.
The simplest way to start sending notifications is using channels. This allows you to use a publisher-subscriber model for sending pushes. Devices start by subscribing to one or more channels, and notifications can later be sent to these subscribers. The channels subscribed to by a given Installation
are stored in the channels
field of the Installation
object.
A channel is identified by a string that starts with a letter and consists of alphanumeric characters, underscores, and dashes. It doesn’t need to be explicitly created before it can be used and each Installation
can subscribe to any number of channels at a time.
Adding a channel subscription can be done using the addUniqueObject:
method in PFObject
. For example, in a baseball score app, we could do:
// When users indicate they are Giants fans, we subscribe them to that channel.
PFInstallation *currentInstallation = [PFInstallation currentInstallation];
[currentInstallation addUniqueObject:@"Giants" forKey:@"channels"];
[currentInstallation saveInBackground];
// When users indicate they are Giants fans, we subscribe them to that channel.
let currentInstallation = PFInstallation.currentInstallation()
currentInstallation.addUniqueObject("Giants", forKey: "channels")
currentInstallation.saveInBackground()
Once subscribed to the “Giants” channel, your Installation
object should have an updated channels
field.
Unsubscribing from a channel is just as easy:
// When users indicate they are no longer Giants fans, we unsubscribe them.
PFInstallation *currentInstallation = [PFInstallation currentInstallation];
[currentInstallation removeObject:@"Giants" forKey:@"channels"];
[currentInstallation saveInBackground];
// When users indicate they are Giants fans, we subscribe them to that channel.
let currentInstallation = PFInstallation.currentInstallation()
currentInstallation.removeObject("Giants", forKey: "channels")
currentInstallation.saveInBackground()
The set of subscribed channels is cached in the currentInstallation
object:
NSArray *subscribedChannels = [PFInstallation currentInstallation].channels;
let subscribedChannels = PFInstallation.currentInstallation().channels
If you plan on changing your channels from Cloud Code or the data browser, note that you’ll need to call some form of fetch
prior to this line in order to get the most recent channels.
While channels are great for many applications, sometimes you need more precision when targeting the recipients of your pushes. Parse allows you to write a query for any subset of your Installation
objects using the querying API and to send them a push.
Since PFInstallation
is a subclass of PFObject
, you can save any data you want and even create relationships between Installation
objects and your other objects. This allows you to send pushes to a very customized and dynamic segment of your user base.
Storing data on an Installation
object is just as easy as storing any other data on Parse. In our Baseball app, we could allow users to get pushes about game results, scores and injury reports.
// Store app language and version
PFInstallation *installation = [PFInstallation currentInstallation];
[installation setObject:@YES forKey:@"scores"];
[installation setObject:@YES forKey:@"gameResults"];
[installation setObject:@YES forKey:@"injuryReports"];
[installation saveInBackground];
// Store app language and version
let installation = PFInstallation.currentInstallation()
installation["scores"] = true
installation["gameResults"] = true
installation["injuryReports"] = true
installation.saveInBackground()
You can even create relationships between your Installation
objects and other classes saved on Parse. To associate a PFInstallation with a particular user, for example, you can simply store the current user on the PFInstallation
.
// Associate the device with a user
PFInstallation *installation = [PFInstallation currentInstallation];
installation[@"user"] = [PFUser currentUser];
[installation saveInBackground];
// Associate the device with a user
let installation = PFInstallation.currentInstallation()
installation["user"] = PFUser.currentUser()
installation.saveInBackground()
It is possible to send arbitrary data along with your notification message, which is explained in the Sending Options section in the JavaScript docs. We can use this data to modify the behavior of your app when a user opens a notification. For example, upon opening a notification saying that a friend commented on a user’s picture, it would be nice to display this picture. This could be done by sending the objectId
of the picture in the push payload and then fetching the image once the user opens the notification.
Due to the package size restrictions imposed by Apple, you need to be careful in managing the amount of extra data sent, since it will cut down on the maximum size of your message. For this reason, it is recommended that you keep your extra keys and values as small as possible.
When an app is opened from a notification, the data is made available in the application:didFinishLaunchingWithOptions:
methods through the launchOptions
dictionary.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
. . .
// Extract the notification data
NSDictionary *notificationPayload = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
// Create a pointer to the Photo object
NSString *photoId = [notificationPayload objectForKey:@"p"];
PFObject *targetPhoto = [PFObject objectWithoutDataWithClassName:@"Photo" objectId:photoId];
// Fetch photo object
[targetPhoto fetchIfNeededInBackgroundWithBlock:^(PFObject *object, NSError *error) {
// Show photo view controller
if (!error) {
PhotoVC *viewController = [[PhotoVC alloc] initWithPhoto:object];
[self.navController pushViewController:viewController animated:YES];
}
}];
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
. . .
// Extract the notification data
if let notificationPayload = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey] as? NSDictionary {
// Create a pointer to the Photo object
let photoId = notificationPayload["p"] as? NSString
let targetPhoto = PFObject(withoutDataWithClassName: "Photo", objectId: photoId)
// Fetch photo object
targetPhoto.fetchIfNeededInBackgroundWithBlock {
(object: PFObject?, error:NSError?) -> Void in
if error == nil {
// Show photo view controller
let viewController = PhotoVC(photo: object);
self.navController.pushViewController(viewController, animated: true);
}
}
}
}
If your app is already running when the notification is received, the data is made available in the application:didReceiveRemoteNotification:fetchCompletionHandler:
method through the userInfo
dictionary.
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))handler {
// Create empty photo object
NSString *photoId = [userInfo objectForKey:@"p"];
PFObject *targetPhoto = [PFObject objectWithoutDataWithClassName:@"Photo" objectId:photoId];
// Fetch photo object
[targetPhoto fetchIfNeededInBackgroundWithBlock:^(PFObject *object, NSError *error) {
// Show photo view controller
if (error) {
handler(UIBackgroundFetchResultFailed);
} else if ([PFUser currentUser]) {
PhotoVC *viewController = [[PhotoVC alloc] initWithPhoto:object];
[self.navController pushViewController:viewController animated:YES];
handler(UIBackgroundFetchResultNewData);
} else {
handler(UIBackgroundFetchResultNoData);
}
}];
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
if let photoId: String = userInfo["p"] as? String {
let targetPhoto = PFObject(withoutDataWithClassName: "Photo", objectId: photoId)
targetPhoto.fetchIfNeededInBackgroundWithBlock { (object: PFObject?, error: NSError?) -> Void in
// Show photo view controller
if error != nil {
completionHandler(UIBackgroundFetchResult.Failed)
} else if PFUser.currentUser() != nil {
let viewController = PhotoVC(withPhoto: object)
self.navController.pushViewController(viewController, animated: true)
completionHandler(UIBackgroundFetchResult.NewData)
} else {
completionHandler(UIBackgroundFetchResult.NoData)
}
}
}
handler(UIBackgroundFetchResult.NoData)
}
You can read more about handling push notifications in Apple’s Local and Push Notification Programming Guide.
To track your users’ engagement over time and the effect of push notifications, we provide some hooks in the PFAnalytics
class. You can view the open rate for a specific push notification on the Parse Dashboard push console. You can also view overall app open and push open graphs are on the Parse analytics console. Our analytics graphs are rendered in real time, so you can easily verify that your application is sending the correct analytics events before your next release.
This section assumes that you’ve already set up your application to save the Installation object. Push open tracking only works when your application’s devices are associated with saved Installation
objects.
First, add the following to your application:didFinishLaunchingWithOptions:
method to collect information about when your application was launched, and what triggered it. The extra checks ensure that, even with iOS 7’s more advanced background push features, a single logical app-open or push-open event is counted as such.
if (application.applicationState != UIApplicationStateBackground) {
// Track an app open here if we launch with a push, unless
// "content_available" was used to trigger a background push (introduced
// in iOS 7). In that case, we skip tracking here to avoid double
// counting the app-open.
BOOL preBackgroundPush = ![application respondsToSelector:@selector(backgroundRefreshStatus)];
BOOL oldPushHandlerOnly = ![self respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)];
BOOL noPushPayload = ![launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if (preBackgroundPush || oldPushHandlerOnly || noPushPayload) {
[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];
}
}
if application.applicationState != UIApplicationState.Background {
// Track an app open here if we launch with a push, unless
// "content_available" was used to trigger a background push (introduced
// in iOS 7). In that case, we skip tracking here to avoid double
// counting the app-open.
let oldPushHandlerOnly = !self.respondsToSelector(Selector("application:didReceiveRemoteNotification:fetchCompletionHandler:"))
let noPushPayload: AnyObject? = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey]?
if oldPushHandlerOnly || noPushPayload != nil {
PFAnalytics.trackAppOpenedWithLaunchOptions(launchOptions)
}
}
Second, if your application is running or backgrounded, the application:didReceiveRemoteNotification:
method handles the push payload instead. If the user acts on a push notification while the application is backgrounded, the application will be brought to the foreground. To track this transition as the application being “opened from a push notification,” perform one more check before calling any tracking code:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
if (application.applicationState == UIApplicationStateInactive) {
// The application was just brought from the background to the foreground,
// so we consider the app as having been "opened by a push notification."
[PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo];
}
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if application.applicationState == .Inactive {
// The application was just brought from the background to the foreground,
// so we consider the app as having been "opened by a push notification."
PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo)
}
}
Finally, if using iOS 7 any of its new push features (including the new “content-available” push functionality), be sure to also implement the iOS 7-only handler:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
if (application.applicationState == UIApplicationStateInactive) {
[PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo];
}
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
if application.applicationState == .Inactive {
PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo)
}
}
If your OS X application supports receiving push notifications and you’d like to track application opens related to pushes, add hooks to the application:didReceiveRemoteNotification:
method (as in iOS) and the following to applicationDidFinishLaunching:
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
// ... other Parse setup logic here
[PFAnalytics trackAppOpenedWithRemoteNotificationPayload:[notification userInfo]];
}
func applicationDidFinishLaunching(notification: NSNotification) {
// ... other Parse setup logic here
PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(notification.userInfo)
}
To track analytics around local notifications, note that application:didReceiveLocalNotification:
is called in addition to application:didFinishLaunchingWithOptions:
, if implemented. Please be careful to prevent tracking duplicate events.
A good time to clear your app’s badge is usually when your app is opened. Setting the badge property on the current installation will update the application icon badge number and ensure that the latest badge value will be persisted to the server on the next save. All you need to do is:
- (void)applicationDidBecomeActive:(UIApplication *)application {
PFInstallation *currentInstallation = [PFInstallation currentInstallation];
if (currentInstallation.badge != 0) {
currentInstallation.badge = 0;
[currentInstallation saveEventually];
}
// ...
}
func applicationDidBecomeActive(application: UIApplication) {
let currentInstallation = PFInstallation.currentInstallation()
if currentInstallation.badge != 0 {
currentInstallation.badge = 0
currentInstallation.saveEventually()
}
// ...
}
The UIApplicationDelegate documentation contains more information on hooks into an app’s life cycle; the ones which are most relevant for resetting the badge count are applicationDidBecomeActive:
, application:didFinishLaunchingWithOptions:
, and application:didReceiveRemoteNotification:
.
You can A/B test your push notifications to figure out the best way to keep your users engaged. With A/B testing, you can simultaneously send two versions of your push notification to different devices, and use each version’s push open rates to figure out which one is better. You can test by either message or send time.
Our web push console guides you through every step of setting up an A/B test.
For each push campaign sent through the Parse web push console, you can allocate a subset of your devices to be in the experiment’s test audience, which Parse will automatically split into two equally-sized experiment groups. For each experiment group, you can specify a different push message. The remaining devices will be saved so that you can send the winning message to them later. Parse will randomly assign devices to each group to minimize the chance for a test to affect another test’s results (although we still don’t recommend running multiple A/B tests over the same devices on the same day).
After you send the push, you can come back to the push console to see in real time which version resulted in more push opens, along with other metrics such as statistical confidence interval. It’s normal for the number of recipients in each group to be slightly different because some devices that we had originally allocated to that experiment group may have uninstalled the app. It’s also possible for the random group assignment to be slightly uneven when the test audience size is small. Since we calculate open rate separately for each group based on recipient count, this should not significantly affect your experiment results.
If you are happy with the way one message performed, you can send that to the rest of your app’s devices (i.e. the “Launch Group”). This step only applies to A/B tests where you vary the message.
Push experiments are supported on all recent Parse SDKs (iOS v1.2.13+, OS X v1.7.5+, Android v1.4.0+, .NET v1.2.7+). Before running experiments, you must instrument your app with push open tracking.
Parse provides guidance on how to run experiments to achieve statistically significant results.
When you setup a push message experiment, we’ll recommend the minimum size of your test audience. These recommendations are generated through simulations based on your app’s historical push open rates. For big push campaigns (e.g. 100k+ devices), this recommendation is usually small subset of your devices. For smaller campaigns (e.g. < 5k devices), this recommendation is usually all devices. Using all devices for your test audience will not leave any remaining devices for the launch group, but you can still gain valuable insight into what type of messaging works better so you can implement similar messaging in your next push campaign.
After you send your pushes to experiment groups, we’ll also provide a statistical confidence interval when your experiment has collected enough data to have statistically significant results. This confidence interval is in absolute percentage points of push open rate (e.g. if the open rates for groups A and B are 3% and 5%, then the difference is reported as 2 percentage points). This confidence interval is a measure of how much difference you would expect to see between the two groups if you repeat the same experiment many times.
Just after a push send, when only a small number of users have opened their push notifications, the open rate difference you see between groups A and B could be due to random chance, so it might not be reproducible if you run the same experiment again. After your experiment collects more data over time, we become increasingly confident that the observed difference is a true difference. As this happens, the confidence interval will become narrower, allowing us to more accurately estimate the true difference between groups A and B. Therefore, we recommend that you wait until there is enough data to generate a statistical confidence interval before deciding which group’s push is better.
Localizing your app’s content is a proven way to drive greater engagement. We’ve made it easy to localize your push messages with Push Localization. The latest version of the Parse iOS SDK will detect and store the user’s language in the installation object, and via the web push console you’ll be able to send localized push messages to your users in a single broadcast.
To take advantage of Push Localization you will need to make sure you’ve published your app with the Parse iOS SDK version 1.8.1 or greater. Any users of your application running the Parse iOS SDK version 1.8.1 or greater will then be targetable by Push Localization via the web push console.
It’s important to note that for developers who have users running apps with versions of the Parse iOS SDK earlier than 1.8.1 that targeting information for Localized Push will not be available and these users will receive the default message from the push console.
Our web push console guides you through every step of setting up a Localized Push.
Setting up Push Notifications is often a source of frustration for developers. The process is complicated and invites problems to happen along the way. We have created a tutorial which covers all the necessary steps to configure your app for push notifications. If you run into issues, try some of the following troubleshooting tips.
It’s important to break down the system into components when troubleshooting push notification issues. You can start by asking yourself the following questions:
If you’re unsure about the answer to any of the above questions, read on!
Having everything set up correctly in your Parse app won’t help if your request to send a push notification does not reach Parse. The first step in debugging a push issue is to confirm that the push campaign is listed in your push logs. You can find these logs by visiting your app’s Dashboard and clicking on Push.
If the push notification campaign is not showing up on that list, the issue is quite simple to resolve. Go back to your push notification sending code and make sure to check for any error responses.
You have confirmed that the push notification is making it to your push logs. Now what? The next step is to verify if your push notification targeting is correct. Say, if you’re debugging why notifications are not reaching a specific device, it’s important to make sure that this device is actually included in the targeting of this push notification. You can’t deliver something that is not addressed correctly.
In order to do this, you’ll have to confirm that the device’s Installation object is included in your push notification targeting criteria. This is quite straightforward if you’re using channels: all you need to verify is that your device’s Installation is subscribed to the channel by checking the channels array. If you’re using advanced targeting, e.g. you’re using a push query with constraints, you’ll have to work a bit more to determine if your targeting is on point.
Basically, you will need to run the same push query you’re using for your targeting, and verify that your installation of interest is included in the result set. As you can only query the installation class programmatically using the Master Key, you will need to use one of the Master Key capable SDKs (JavaScript SDK, .NET) or the REST API. Ideally, you would use the same SDK that you’re currently using to send the push notifications.
The REST API is quite easy to use for this sort of purpose as you can easily recreate the push query using the information provided in your push notification logs. If you look closely at the “Full Target” value in your push campaign log item, you may notice that it matches the query format for a REST API query. You can grab an example of what a REST API query over Installations would look like from the REST API docs. Don’t forget to use the X-Parse-Master-Key
header to ensure that the Master Key is used to run this query.
# Query over installations
curl -X GET \
-H "X-Parse-Application-Id: {YOUR_APPLICATION_ID}" \
-H "X-Parse-Master-Key: {YOUR_MASTER_KEY}" \
-G \
--data-urlencode 'limit=1000' \
--data-urlencode 'where={ "city": "San Francisco", "deviceType": { "$in": [ "ios", "android", "winphone", "embedded" ] } }' \
https://YOUR.PARSE-SERVER.HERE/parse/installations
If you type the above into a console, you should be able to see the first 1,000 objects that match your query. Note that constraints are always ANDed, so if you want to further reduce the search scope, you can add a constraint that matches the specific installation for your device:
# Query over installations
curl -X GET \
-H "X-Parse-Application-Id: {YOUR_APPLICATION_ID}" \
-H "X-Parse-Master-Key: {YOUR_MASTER_KEY}" \
-G \
--data-urlencode 'limit=1' \
--data-urlencode 'where={ “objectId”: {YOUR_INSTALLATION_OBJECT_ID}, "city": "San Francisco", "deviceType": { "$in": [ "ios", "android", "winphone", "embedded" ] } }' \
https://YOUR.PARSE-SERVER.HERE/parse/installations
If the above query returns no results, it is likely that your installation does not meet the targeting criteria for your campaign.
Your push campaign is created and the device is included in the targeting, but you’re still not receiving push notifications. What gives?
You can check the Push Delivery Report for the cause of failed deliveries: Invalid Tokens happens when the user have uninstalled the app or if it is misconfigured, No Certificates is returned when all available certificates were rejected by Apple.
This is a good time to go through your project settings and make sure everything is in order.
Info.plist
file in your Xcode project. This should match your push certificate bundle identifier as well as your Installation object’s bundle identifier.Project > Build Settings
in Xcode.If everything compiles and runs with no errors, but you are still not receiving pushes:
Settings > Notification > YourAppName
.If your app has been released for a while, it is expected that a percentage of your user base will have opted out of push notifications from your app or uninstalled your app from their device. Parse does not automatically delete installation objects in either of these cases. When a push campaign is sent out, Parse will detect uninstalled installations and exclude them from the total count of push notifications sent. The recipient estimate on the push composer is based on the estimated number of installations that match your campaign’s targeting criteria. With that in mind, it is possible for the recipient estimate to be higher than the number of push notifications that is sent as reported by the push campaign status page.
If everything looks great so far, but push notifications are not showing up on your phone, there are a few more things you can check. For example, if your app is in the foreground when the push notification is received, an alert will not be displayed by default. You will need to handle the incoming push notification and perform the necessary action as documented in Responding to the Payload.
If your app is not running in the foreground and the push notification is not showing up, make sure that you’re specifying an “alert” key in your payload. Otherwise, the push notification will be treated as a silent push notification.
When using content-available
to send silent push notifications, keep in mind that APNS may throttle push notifications sent to the same device token within a short period of time.
PFConfig
is a way to configure your applications remotely by storing a single configuration object on Parse. It enables you to add things like feature gating or a simple “Message of the Day”. To start using PFConfig
you need to add a few key/value pairs (parameters) to your app on the Parse Config Dashboard.
After that you will be able to fetch the PFConfig
on the client, like in this example:
[PFConfig getConfigInBackgroundWithBlock:^(PFConfig *config, NSError *error) {
NSNumber *number = config[@"winningNumber"];
NSLog(@"Yay! The number is %@!", [number stringValue]);
}];
PFConfig.getConfigInBackgroundWithBlock {
(config: PFConfig?, error: NSError?) -> Void in
let number = config?["winningNumber"] as? Int
print("Yay! The number is \(number)!")
}
PFConfig
is built to be as robust and reliable as possible, even in the face of poor internet connections. Caching is used by default to ensure that the latest successfully fetched config is always available. In the below example we use getConfigInBackgroundWithBlock
to retrieve the latest version of config from the server, and if the fetch fails we can simply fall back to the version that we successfully fetched before via currentConfig
.
NSLog(@"Getting the latest config...");
[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!";
}
NSLog(@"Welcome Messsage = %@", welcomeMessage);
}];
print("Getting the latest config...");
PFConfig.getConfigInBackgroundWithBlock {
(var config: PFConfig?, error: NSError?) -> Void in
if error == nil {
print("Yay! Config was fetched from the server.")
} else {
print("Failed to fetch. Using Cached Config.")
config = PFConfig.currentConfig()
}
var welcomeMessage: NSString? = config?["welcomeMessage"] as? NSString
if let welcomeMessage = welcomeMessage {
print("Welcome Message = \(welcomeMessage)!")
} else {
print("Falling back to default message.")
welcomeMessage = "Welcome!";
}
};
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 [PFConfig currentConfig]
. Therefore, you can safely pass around any PFConfig
object and safely assume that it will not automatically change.
It might be troublesome to retrieve the config from the server every time you want to use it. You can avoid this by simply using the cached currentConfig
object and fetching the config only once in a while.
// Fetches the config at most once every 12 hours per app runtime
const NSTimeInterval configRefreshInterval = 12.0 * 60.0 * 60.0;
static NSDate *lastFetchedDate;
if (lastFetchedDate == nil ||
[lastFetchedDate timeIntervalSinceNow] * -1.0 > configRefreshInterval) {
[PFConfig getConfigInBackgroundWithBlock:nil];
lastFetchedDate = [NSDate date];
}
// Fetches the config at most once every 12 hours per app runtime
let configRefreshInterval: NSTimeInterval = 12.0 * 60.0 * 60.0
struct DateSingleton {
static var lastFetchedDate: NSDate? = nil
}
let date: NSDate? = DateSingleton.lastFetchedDate;
if date == nil ||
date!.timeIntervalSinceNow * -1.0 > configRefreshInterval {
PFConfig.getConfigInBackgroundWithBlock(nil);
DateSingleton.lastFetchedDate = NSDate();
}
PFConfig
supports most of the data types supported by PFObject
:
We currently allow up to 100 parameters in your config and a total size of 128KB across all parameters.
Parse provides a number of hooks for you to get a glimpse into the ticking heart of your app. We understand that it’s important to understand what your app is doing, how frequently, and when.
While this section will cover different ways to instrument your app to best take advantage of Parse’s analytics backend, developers using Parse to store and retrieve data can already take advantage of metrics on Parse.
Without having to implement any client-side logic, you can view real-time graphs and breakdowns (by device type, Parse class name, or REST verb) of your API Requests in your app’s dashboard and save these graph filters to quickly access just the data you’re interested in.
Our initial analytics hook allows you to track your application being launched. By adding the following line to applicationDidFinishLaunching:
, you’ll begin to collect data on when and how often your application is opened.
// in iOS
[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];
// in OS X
[PFAnalytics trackAppOpenedWithLaunchOptions:nil];
// in iOS
PFAnalytics.trackAppOpened(launchOptions: launchOptions)
// in OS X
PFAnalytics.trackAppOpenedWithLaunchOptions(nil)
PFAnalytics
also allows you to track free-form events, with a handful of NSString
keys and values. These extra dimensions allow segmentation of your custom events via your app’s Dashboard.
Say your app offers search functionality for apartment listings, and you want to track how often the feature is used, with some additional metadata.
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];
let 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)
PFAnalytics
can even be used as a lightweight error tracker — simply invoke the following and you’ll have access to an overview of the rate and frequency of errors, broken down by error code, in your application:
NSString *codeString = [NSString stringWithFormat:@"%d", [error code]];
[PFAnalytics trackEvent:@"error" dimensions:@{ @"code": codeString }];
let codeString = NSString(format:"%@", error.code)
PFAnalytics.trackEvent("error", dimensions:["code": codeString])
Note that Parse currently only stores the first eight dimension pairs per call to trackEvent:dimensions:
.
At the end of the day, users of your app are going to be interacting with UIKit components.
ParseUI is an opensource collection of a handy user interface components aimed to streamline and simplify user authentication, displaying lists of data, and other common app elements.
ParseUI can be installed by leveraging Cocoapods ‘subspecs’, simply add pod 'Parse/UI'
to your Podfile and run pod install
. Once installed just use import Parse
to use ParseUI. More details can be found on the official GitHub page
If you are using Parse to manage users in your mobile app, you are already familiar with the PFUser
class. At some point in your app, you might want to present a screen to log in your PFUser
. ParseUI
provides a view controller that does exactly this:
You use the PFLogInViewController
class by instantiating it and presenting it modally:
PFLogInViewController *logInController = [[PFLogInViewController alloc] init];
logInController.delegate = self;
[self presentViewController:logInController animated:YES completion:nil];
var logInController = PFLogInViewController()
logInController.delegate = self
self.present(logInController, animated:true, completion: nil)
PFLogInViewController
can be configured to provide a variety of log in options. By default, PFLogInViewController
presents the following UI:
Any of the above features can be turned on or off. The options can be set using the fields
property on PFLogInViewController
:
logInController.fields = (PFLogInFieldsUsernameAndPassword
| PFLogInFieldsLogInButton
| PFLogInFieldsSignUpButton
| PFLogInFieldsPasswordForgotten
| PFLogInFieldsDismissButton);
logInController.fields = [PFLogInFields.usernameAndPassword,
PFLogInFields.logInButton,
PFLogInFields.signUpButton,
PFLogInFields.passwordForgotten,
PFLogInFields.dismissButton]
Essentially, you create an array of all the options you want to include in the log in screen, and assign the value to fields
.
In addition, there are a number of other options that can be turned on, including:
Similarly, you can turn on Facebook or Twitter log in as such:
logInController.fields = (PFLogInFieldsUsernameAndPassword
| PFLogInFieldsFacebook
| PFLogInFieldsTwitter);
logInController.fields = [PFLogInFields.usernameAndPassword,
PFLogInFields.facebook,
PFLogInFields.twitter]
The above code would produce a log in screen that includes username, password, Facebook and Twitter buttons. Facebook log in permissions can be set via the facebookPermissions
.
PFLogInViewController *logInController = [[PFLogInViewController alloc] init];
logInController.delegate = self;
logInController.facebookPermissions = @[ @"friends_about_me" ];
[self presentViewController:logInController animated:YES completion:nil];
var logInController = PFLogInViewController()
logInController.delegate = self
logInController.facebookPermissions = [ "friends_about_me" ]
self.present(logInController, animated:true, completion:nil)
When the user signs in or cancels, the PFLogInViewController
notifies the delegate of the event. Upon receiving this callback, the delegate should, at a minimum, dismiss PFLogInViewController
. Additionally, the delegate could possibly update its own views or forward the message to the other components that need to know about the PFUser
.
- (void)logInViewController:(PFLogInViewController *)controller
didLogInUser:(PFUser *)user {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)logInViewControllerDidCancelLogIn:(PFLogInViewController *)logInController {
[self dismissViewControllerAnimated:YES completion:nil];
}
func logInViewController(controller: PFLogInViewController, didLogInUser user: PFUser!) -> Void {
self.dismiss(animated: true, completion: nil)
}
func logInViewControllerDidCancelLog(in controller: PFLogInViewController) -> Void {
self.dismiss(animated: true, completion: nil)
}
Besides the delegate pattern, the PFLogInViewController
also supports NSNotification
s, which is useful if there are multiple observers of the sign in events.
You might want to use your own logo or background image. You can achieve this by subclassing PFLogInViewController
and overriding viewDidLoad
method:
@interface MyLogInViewController : PFLogInViewController
@end
@implementation MyLogInViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor darkGrayColor];
UIImageView *logoView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"logo.png"]];
self.logInView.logo = logoView; // logo can be any UIView
}
@end
class MyLogInViewController: PFLogInViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .darkGray
let logoView = UIImageView(image: UIImage(named:"logo.png"))
self.logInView?.logo = logoView
}
}
If you would like to modify the logo and the background of the associated sign up view, you will need to subclass PFSignUpViewController
and create an instance of the subclass and assign it to the signUpController
as soon as you instantiate PFLogInViewController
:
MyLogInViewController *logInController = [[MyLogInViewController alloc] init];
logInController.signUpController = [[MySignUpViewController alloc] init];
[self presentViewController:logInController animated:YES completion:nil];
let logInController = MyLogInViewController()
logInController.signUpController = MySignUpViewController()
self.present(logInController, animated: true, completion: nil)
Occasionally you might want to customize PFLogInViewController
further. For example, you might want to change the placeholder text to “Email” or change the size of the login button. In both cases, you need to subclass PFLogInViewController
and override either viewDidLoad
or viewDidLayoutSubviews
. Override the former if the behavior is not related to layout, and override the latter otherwise:
@interface MyLogInViewController : PFLogInViewController
@end
@implementation MyLogInViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.logInView.usernameField.placeholder = @"email";
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.logInView.logInButton.frame = CGRectMake(...); // Set a different frame.
}
@end
class MyLogInViewController : PFLogInViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.logInView.usernameField.placeholder = "email"
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.logInView.logInButton.frame = CGRectMake(...) // Set a different frame.
}
}
Developers interested in this kind of customization should take a look at the interface of PFLogInView
, where all customizable properties are documented.
By default, the PFLogInViewController
supports all orientations on iPad and UIInterfaceOrientationPortrait
on iPhone.
The PFLogInViewController
is written to be resolution-independent, meaning it looks great on all iOS device sizes and pixel densities.
If you are using PFLogInViewController
with the PFLogInFieldsSignUpButton
option enabled, you do not need to do any additional work to enable the sign up functionality. When your user taps on the sign up button on the log in screen, a sign up screen will appear and allow them to sign up. However, there are occasions where you might want to use the sign up screen independently of the log in screen. This is when the PFSignUpViewController
comes in handy.
You use PFSignUpViewController
by instantiating it and presenting it modally:
PFSignUpViewController *signUpController = [[PFSignUpViewController alloc] init];
signUpController.delegate = self;
[self presentViewController:signUpController animated:YES completion:nil];
let signUpController = PFSignUpViewController()
signUpController.delegate = self
self.present(signUpController, animated: true, completion: nil)
That is all you need to do to get a functional sign up screen.
PFSignUpViewController
can be configured to provide a variety of sign up options. By default, it presents the following UI:
If your sign up screen requires an additional field on top of the default ones, such as “phone number”, you can turn on a field called named “additional”:
signUpController.fields = (PFSignUpFieldsUsernameAndPassword
| PFSignUpFieldsSignUpButton
| PFSignUpFieldsEmail
| PFSignUpFieldsAdditional
| PFSignUpFieldsDismissButton);
signUpController.fields = [.usernameAndPassword, .signUpButton, .email, .additional, .dismissButton]
Essentially, you create an array (in Swift), or use the bitwise or operator (for objective-c), to chain all of the options you want to include in the sign up screen, and assign the value to fields
. Similarly, you can turn off any field by omitting it in the assignment to fields.
When the user signs up or cancels, the PFSignUpViewController
notifies the delegate of the event. Upon receiving this callback, the delegate should, at a minimum, dismiss PFSignUpViewController
. Additionally, the delegate could update its own views or forward the message to the other components that need to know about the PFUser
.
- (void)signUpViewController:(PFSignUpViewController *)signUpController didSignUpUser:(PFUser *)user {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)signUpViewControllerDidCancelSignUp:(PFSignUpViewController *)signUpController {
[self dismissViewControllerAnimated:YES completion:nil];
}
func signUpViewController(signUpController: PFSignUpViewController, didSignUpUser user: PFUser) -> Void {
self.dismiss(animated: true, completion: nil)
}
func signUpViewControllerDidCancelSignUp(signUpController: PFSignUpViewController) -> Void {
self.dismiss(animated: true, completion: nil)
}
Besides the delegate pattern, the PFSignUpViewController
also supports NSNotification
s, which is useful when there are multiple listeners of the sign up events.
You might want to use your own logo or background image. You can achieve this by subclassing PFSignUpViewController
and overriding viewDidLoad
:
@interface MySignUpViewController : PFSignUpViewController
@end
@implementation MySignUpViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor darkGrayColor];
UIImageView *logoView = [[UIImageView alloc] initWithImage:@"logo.png"];
self.signUpView.logo = logoView; // logo can be any UIView
}
@end
class MySignUpViewController : PFSignUpViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .darkGray
let logoView = UIImageView(image: UIImage(named: "logo.png"))
self.signUpView?.logo = logoView // 'logo' can be any UIView
}
}
Often you will want to run some client-side validation on the sign up information before submitting it to the Parse Cloud. You can add your validation logic in the signUpViewController:shouldBeginSignUp:
method in the PFSignUpViewControllerDelegate
. For example, if you decide any password less than 8 characters is too short, you can achieve the following with:
- (BOOL)signUpViewController:(PFSignUpViewController *)signUpController
shouldBeginSignUp:(NSDictionary *)info {
NSString *password = info[@"password"];
return (password.length >= 8); // prevent sign up if password has to be at least 8 characters long
}
func signUpViewController(_ signUpController: PFSignUpViewController,
shouldBeginSignUp info: [String : String]) -> Bool {
if let password = info["password"] {
return password.utf16.count >= 8
}
return false
}
info
is a dictionary that contains all sign up fields, such as username, password, email, and additional.
Occasionally you might want to customize PFSignUpViewController
further. For example, you might want to change the “additional” placeholder text to “Phone” or change the size of the signup button. You can always subclass PFSignUpViewController
and override UIViewController
’s various methods. You should override the viewDidLoad
if the behavior you want to change is unrelated to view layout, and override viewDidLayoutSubviews
otherwise:
@interface MySignUpViewController : PFSignUpViewController
@end
@implementation MySignUpViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.signUpView.usernameField.placeholder = @"phone";
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.signUpView.signUpButton.frame = CGRectMake(...); // Set a different frame.
}
@end
class MySignUpViewController : PFSignUpViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.signUpView?.usernameField?.placeholder = "phone"
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.signUpView?.signUpButton?.frame = CGRectMake(...) // Set a different frame.
}
}
Developers interested in this kind of customization should take a look at the interface of PFSignUpView
, where all customizable properties are documented.
By default, the PFSignUpViewController
supports all orientations on iPad and UIInterfaceOrientationPortrait
on iPhone.
The PFSignUpViewController
is written to be resolution-independent, meaning it looks great on all iOS device sizes and pixel densities.
Data oriented iOS applications are mostly a collection of UITableViewController
s and corresponding UITableView
s. When using Parse, each cell of a UITableView
typically represents data from a PFObject
. PFQueryTableViewController
is a sub-class of UITableViewController
that provides a layer of abstraction that lets you easily display data from one of your Parse classes.
You use PFQueryTableViewController
much like how you would use UITableViewController
:
PFQueryTableViewController
and customize it. Use the template file as a starting point.parseClassName
instance variable to specify which Parse class should be queried for data.queryForTable
method to construct a custom PFQuery
that should be used to get objects for the table.tableView:cellForRowAtIndexPath:object:
method to return a custom cell tailored for each PFObject
.PFTableViewCell
class.PFObject
s via the constructed query and loads it into the table. It even includes pagination and pull-to-refresh out of the box.The class allows you to think about a one-to-one mapping between a PFObject
and a UITableViewCell
, rather than having to juggle index paths. You also get the following features out of the box:
The easiest way to understand this class is with an example. This subclass of PFQueryTableViewController
displays a series of Todo items and their numeric priorities:
@interface SimpleTableViewController : PFQueryTableViewController
@end
@implementation SimpleTableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) { // This table displays items in the Todo class
self.parseClassName = @"Todo";
self.pullToRefreshEnabled = YES;
self.paginationEnabled = YES;
self.objectsPerPage = 25;
}
return self;
}
- (PFQuery *)queryForTable {
PFQuery *query = [PFQuery queryWithClassName:self.parseClassName];
// If no objects are loaded in memory, we look to the cache first to fill the table
// and then subsequently do a query against the network.
if (self.objects.count == 0) {
query.cachePolicy = kPFCachePolicyCacheThenNetwork;
}
[query orderByDescending:@"createdAt"];
return query;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
object:(PFObject *)object {
static NSString *cellIdentifier = @"cell";
PFTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[PFTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:cellIdentifier];
}
// Configure the cell to show todo item with a priority at the bottom
cell.textLabel.text = object[@"text"];
cell.detailTextLabel.text = [NSString stringWithFormat:@"Priority: %@", object[@"priority"]];
return cell;
}
@end
class SimpleTableViewController: PFQueryTableViewController {
override init(style: UITableView.Style, className: String?) {
super.init(style: style, className: className)
parseClassName = "Todo"
pullToRefreshEnabled = true
paginationEnabled = true
objectsPerPage = 25
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
parseClassName = "Todo"
pullToRefreshEnabled = true
paginationEnabled = true
objectsPerPage = 25
}
override func queryForTable() -> PFQuery<PFObject> {
let query = PFQuery(className: self.parseClassName!)
// If no objects are loaded in memory, we look to the cache first to fill the table
// and then subsequently do a query against the network.
if self.objects!.count == 0 {
query.cachePolicy = .cacheThenNetwork
}
query.order(byDescending: "createdAt")
return query
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
let cellIdentifier = "cell"
var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as? PFTableViewCell
if cell == nil {
cell = PFTableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
}
// Configure the cell to show todo item with a priority at the bottom
if let object = object {
cell!.textLabel?.text = object["text"] as? String
let priority = object["priority"] as? String
cell!.detailTextLabel?.text = "Priority \(String(describing: priority))"
}
return cell
}
}
This view shows a list of Todo items and also allows the user to pull-to-refresh and load the next page by touching a special pagination cell at the end of the table. It also properly caches the objects such that when the view is no longer in memory, the next time it loads it will use the query cache to immediately show the previously loaded objects while making a network call to update.
Notice all the code that we’re not writing. We don’t need to handle loading the data into the table, wrangle index paths, or handle tricky pagination code. That’s all handled by the PFQueryTableViewController
automatically.
A good starting point to learn more is to look at the API for the class and also the template subclass file. We designed the class with customizability in mind, so it should accommodate many instances where you used to use UITableViewController
.
PFQueryTableViewController
makes it simple to display remote images stored in the Parse Cloud as PFFileObject
s. All you need to do is to override tableView:cellForRowAtIndexPath:object:
and return a PFTableViewCell
with its imageView
’s file
property specified. If you would like to display a placeholder image to be shown before the remote image is loaded, assign the placeholder image to the image
property of the imageView
.
@implementation SimpleTableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object {
static NSString *identifier = @"cell";
PFTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) { cell = [[PFTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
cell.textLabel.text = object[@"title"];
PFFileObject *thumbnail = object[@"thumbnail"];
cell.imageView.image = [UIImage imageNamed:@"placeholder.jpg"];
cell.imageView.file = thumbnail;
return cell;
}
@end
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
let identifier = "cell"
var cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? PFTableViewCell
if cell == nil {
cell = PFTableViewCell(style: .default, reuseIdentifier: identifier)
}
if let object = object {
cell?.textLabel?.text = object["title"] as? String
cell?.imageView?.image = UIImage(named: "placeholder.jpg")
cell?.imageView?.file = object["thumbnail"] as? PFFileObject
}
return cell
}
This table shows a list of cute animal photos which are stored in the Parse Cloud, as PFFileObject
s. “placeholder.jpg” is an image included in the application bundle which is shown before the animal photos are downloaded.
The images are downloaded on demand. As you scroll through the table, the images in the currently visible cells are downloaded. This just-in-time behavior is desirable because not only does it conserve bandwidth, it also ensures timely display of visible images. If a more aggressive loading behavior is desired, you can use the loadInBackground
method on imageView
to download the image.
The default query is set to get objects from your class ordered by descending createdAt
. To customize, simply override the queryForTable
method to return your own PFQuery
. The table will use this query when getting objects to display.
To customize the look of your table, override tableView:cellForRowAtIndexPath:object:
to return a customized cell. Notice that this method is similar to the typical table data source method, but it includes the PFObject
directly as a parameter.
You should no longer override tableView:cellForRowAtIndexPath:
.
Important: your table view cells should inherit from PFTableViewCell
, rather than UITableViewCell
. PFTableViewCell
is a subclass of UITableViewCell
that supports remote image loading. When used in PFQueryTableViewController
, PFTableViewCell
’s remote images would be automatically loaded.
Several methods are exposed that are called at major events during the data lifecycle of the table. They are objectsDidLoad:
and objectsWillLoad
, which are called after the objects have loaded from the query, and right before the query is fired, respectively. You can override these to provide extra functionality during these events.
Pagination ensures that the table only gets one page of objects at a time. You can customize how many objects are in a page by setting the objectsPerPage
instance variable.
The query is automatically altered to apply pagination, and, when the table first loads, it only shows the first page of objects. A pagination cell appears at the bottom of the table which allows users to load the next page. You can customize this cell by overriding tableView:cellForNextPageAtIndexPath:
Pagination is turned on by default. If you want to turn it off, simply set paginationEnabled
to NO
.
Pull to Refresh is a feature that allows users to pull the table down and release to reload the data. Essentially, the first page of data is reloaded from your class and the table is cleared and updated with the data.
Pull to Refresh is turned on by default. If you want to turn it off, simply set pullToRefreshEnabled
to NO
.
A loading view is displayed when the table view controller is loading the first page of data. It is turned on by default, and can be turned off via the property loadingViewEnabled
.
When the user is offline or a Parse error was generated from a query, an alert can automatically be shown to the user. By default, this is turned on when using PFQueryTableViewController
. If you want to turn this behavior off, you can do so using the methods offlineMessagesEnabled
and errorMessagesEnabled
on the Parse
class.
Many apps need to display images stored in the Parse Cloud as PFFileObject
s. However, to load remote images with the built-in UIImageView
involves writing many lines of boilerplate code. PFImageView
simplifies this task:
PFImageView *imageView = [[PFImageView alloc] init];
imageView.image = [UIImage imageNamed:@"..."]; // placeholder image
imageView.file = (PFFileObject *)someObject[@"picture"]; // remote image
[imageView loadInBackground];
let imageView = PFImageView()
imageView.image = UIImage(named: "...") // placeholder image
imageView.file = someObject.picture // remote image
imageView.loadInBackground()
If assigned to, the image
property is used to display a placeholder before the remote image is downloaded. Note that the download does not start as soon as the file
property is assigned to, but the loading only begins when loadInBackground:
is called. The remote image is cached both in memory and on disc. If the image is found in cache, the call to loadInBackground:
would return immediately.
Many apps need to display table view cells which contain images stored in the Parse Cloud as PFFileObject
s. However, to load remote images with the built-in UITableViewCell
involves writing many lines of boilerplate code. PFTableViewCell
simplifies this task by exposing an imageView
property of the type PFImageView
that supports remote image loading:
@implementation SimpleTableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object {
static NSString *identifier = @"cell";
PFTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) { cell = [[PFTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
cell.textLabel.text = object[@"title"];
PFFileObject *thumbnail = object[@"thumbnail"];
cell.imageView.image = [UIImage imageNamed:@"placeholder.jpg"];
cell.imageView.file = thumbnail;
return cell;
}
@end
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let identifier = "cell"
var cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? PFTableViewCell
if cell == nil {
cell = PFTableViewCell(style: .default, reuseIdentifier: identifier)
}
if let title = object["title"] as? String {
cell!.textLabel.text = title
}
if let thumbnail = object["thumbnail"] as? PFFileObject {
cell!.imageView?.image = UIImage(named: "placeholder.jpg")
cell!.imageView.file = thumbnail
}
return cell!
}
Like UITableViewCell
, PFTableViewCell
supports the default layout styles. Unlike UITableViewCell
, PFTableViewCell
’s imageView
property is of the type PFImageView
, which supports downloading remote images in PFFileObject
.
Although it can be used independently, PFTableViewCell
really shines when used in PFQueryTableViewController
. PFQueryTableViewController
knows about PFTableViewCell
and loads the images automatically. This behavior is discussed in detail in the documentation for PFQueryTableViewController
.
All strings in Parse’s UI classes are customizable/localizable. The easiest way to customize a string is through the default localization support provided by iOS.
Say, for example, you would like to customize the loading message in the HUD of PFSignUpViewController
that says “Loading…” Assume you have followed the localization guide and set up Localizable.strings
in the en.lproj
directory. In Localizable.strings
, you can then enter:
"Loading..." = "In progress";
That would customize the string to “In progress”. The key on the left is the original string you want to customize, and the value on the right is the customized value.
Say, you would like to customize the error message in PFSignUpViewController
that says “The email address “andrew@x” is invalid. Please enter a valid email.” You are not sure how to enter this into Localizable.strings
because it contains a variable.
Included in the Parse SDK is a file named Localizable.string
which includes all the localizable keys in the Parse framework. Browsing this file, developers can find the key for the string they would like to customize. You notice that the string "The email address \"%@\" is invalid. Please enter a valid email."
is a key in the file. In your own Localizable.strings
, you can then enter:
"The email address \"%@\" is invalid. Please enter a valid email." = "Wrong email: \"%@\"";
The string is now customized.
Parse provides a set of APIs for working with in-app purchases. Parse makes it easier to work with StoreKit
and facilitates delivery of downloadable content with receipt verification in the cloud. Receipt verification is a mechanism that allows you to restrict downloads to only those users that have paid accordingly.
In addition, developers can attach query-able metadata on products to categorize, search, and dynamically manipulate products available for purchase.
Lastly, any content uploaded to Parse is exempt from the Apple review process, and hence can be served as soon as the upload is complete.
Prior to using in-app purchases on Parse, you’ll need to set up your app and products with Apple. This process spans both the provisioning portal and iTunes Connect. We recommend following this step-by-step guide.
Note that this is a tricky setup process so please ensure you follow Apple’s documentation precisely.
Once the setup above is complete, you can begin working with in-app purchases.
On the main thread, register the handlers for the products:
// Use the product identifier from iTunes to register a handler.
[PFPurchase addObserverForProduct:@"Pro" block:^(SKPaymentTransaction *transaction) {
// Write business logic that should run once this product is purchased.
isPro = YES;
}];
// Use the product identifier from iTunes to register a handler.
PFPurchase.addObserverForProduct("Pro") {
(transaction: SKPaymentTransaction?) -> Void in
// Write business logic that should run once this product is purchased.
isPro = YES;
}
Note that this does not make the purchase, but simply registers a block to be run if a purchase is made later. This registration must be done on the main thread, preferably as soon as the app is launched, i.e. in application:didFinishLaunchingWithOptions:
. If there are multiple products, we recommend registering all product handlers in the same method, such as application:didFinishLaunchingWithOptions
:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...
[PFPurchase addObserverForProduct:@"Pro" block:^(SKPaymentTransaction *transaction) {
isPro = YES;
}];
[PFPurchase addObserverForProduct:@"VIP" block:^(SKPaymentTransaction *transaction) {
isVip = YES;
}];
}
PFPurchase.addObserverForProduct("Pro") {
(transaction: SKPaymentTransaction?) -> Void in
isPro = YES;
}
PFPurchase.addObserverForProduct("VIP") {
(transaction: SKPaymentTransaction?) -> Void in
isVip = YES;
}
To initiate a purchase, use the +[PFPurchase buyProduct:block:]
method:
[PFPurchase buyProduct:@"Pro" block:^(NSError *error) {
if (!error) {
// Run UI logic that informs user the product has been purchased, such as displaying an alert view.
}
}];
PFPurchase.buyProduct("Pro") {
(error: NSError?) -> Void in
if error == nil {
// Run UI logic that informs user the product has been purchased, such as displaying an alert view.
}
}
The call to buyProduct:block:
brings up a dialogue that asks users to enter their Apple credentials. When the user’s identity is verified, the product will be purchased. If the product is non-consumable and has been purchased by the user before, the user will not be charged.
Many IAP products such as books and movies have associated content files that should be downloaded once the purchase is made. This is very simple to do with Parse:
Product
class,productIdentifier:
the product identifier of the product, matching the one in iTunes Connect</code>icon:
the icon to be displayed in PFProductTableViewController
title:
the title to be displayed in PFProductTableViewController
subtitle:
the subtitle to be displayed in PFProductTableViewController
order
: the order this product should appear in PFProductTableViewController
. This is used only in PFProductTableViewController
; fill in 0 if the order is not important,download
: the downloadable content file. Note that the file uploaded in download
is not publicly accessible, and only becomes available for download when a purchase is made. downloadName
is the name of the file on disk once downloaded. You don’t need to fill this in.[PFPurchase addObserverForProduct:@"Pro" block:^(SKPaymentTransaction *transaction) {
[PFPurchase downloadAssetForTransaction:transaction completion:^(NSString *filePath, NSError *error) {
if (!error) {
// at this point, the content file is available at filePath.
}
}];
}];
PFPurchase.addObserverForProduct("Pro") {
(transaction: SKPaymentTransaction?) -> Void in
if let transaction = transaction {
PFPurchase.downloadAssetForTransaction(transaction) {
(filePath: String?, error: NSError?) -> Void in
if error == nil {
// at this point, the content file is available at filePath.
}
}
}
}
Note that this does not make the purchase, but simply registers a block to be run if a purchase is made later. The call to downloadAssetForTransaction:completion:
passes the receipt of the purchase to the Parse Cloud, which then verifies with Apple that the purchase was made. Once the receipt is verified, the purchased file is downloaded.
To make the purchase:
[PFPurchase buyProduct:@"Pro" block:^(NSError *error) {
if (!error) {
// run UI logic that informs user the product has been purchased, such as displaying an alert view.
}
}];
PFPurchase.buyProduct("Pro") {(error: NSError?) -> Void in
if error == nil {
// run UI logic that informs user the product has been purchased, such as displaying an alert view.
}
}
The call to buyProduct:block:
brings up a dialogue that asks users to enter their Apple credentials. When the user’s identity is verified, the product will be purchased.
You can query the product objects created in the data browser using PFProduct
. Like PFUser
or PFRole
, PFProduct
is a subclass of PFObject
that contains convenience accessors to various product-specific properties.
For example, here’s a simple query to get a product:
PFQuery *productQuery = [PFProduct query];
PFProduct *product = [[productQuery findObjects] lastObject];
NSLog(@"%@, %@", product.productIdentifier, product.title);
let productQuery = PFProduct.query()
if let product = productQuery.findObjects.lastObject as? PFProduct {}
print(product.productIdentifier, product.title)
}
PFProductTableViewController
is a subclass of PFQueryTableViewController
that displays all IAP products in a table view. Some content apps, such as an app that sells comic books or video tutorials, may find it handy to use PFProductTableViewController
to sell the products. By default, each cell is a product, and tapping on a cell initiates the purchase for the product. If the product has associated downloadable content, the download will start when the cell is selected and a progress bar is displayed to indicate the progress of the download.
Note that in order to use this class, you must enter all product information in the Product
class via the data browser.
We’ve designed the Parse SDKs so that you typically don’t need to worry about how data is saved while using the client SDKs. Simply add data to the Parse Object
, and it’ll be saved correctly.
Nevertheless, there are some cases where it’s useful to be aware of how data is stored on the Parse platform.
Internally, Parse stores data as JSON, so any datatype that can be converted to JSON can be stored on Parse. Refer to the Data Types in Objects section of this guide to see platform-specific examples.
Keys including the characters $
or .
, along with the key __type
key, are reserved for the framework to handle additional types, so don’t use those yourself. Key names must contain only numbers, letters, and underscore, and must start with a letter. Values can be anything that can be JSON-encoded.
When a class is initially created, it doesn’t have an inherent schema defined. This means that for the first object, it could have any types of fields you want.
However, after a field has been set at least once, that field is locked into the particular type that was saved. For example, if a User
object is saved with field name
of type String
, that field will be restricted to the String
type only (the server will return an error if you try to save anything else).
One special case is that any field can be set to null
, no matter what type it is.
The Data Browser is the web UI where you can update and create objects in each of your apps. Here, you can see the raw JSON values that are saved that represents each object in your class.
When using the interface, keep in mind the following:
objectId
, createdAt
, updatedAt
fields cannot be edited (these are set automatically).null
).The Data Browser is also a great place to test the Cloud Code validations contained in your Cloud Code functions (such as beforeSave
). These are run whenever a value is changed or object is deleted from the Data Browser, just as they would be if the value was changed or deleted from your client code.
You may import data into your Parse app by using CSV or JSON files. To create a new class with data from a CSV or JSON file, go to the Data Browser and click the “Import” button on the left hand column.
The JSON format is an array of objects in our REST format or a JSON object with a results
that contains an array of objects. It must adhere to the JSON standard. A file containing regular objects could look like:
{ "results": [
{
"score": 1337,
"playerName": "Sean Plott",
"cheatMode": false,
"createdAt": "2022-01-01T12:23:45.678Z",
"updatedAt": "2022-01-01T12:23:45.678Z",
"objectId": "fchpZwSuGG"
}]
}
Objects in either format should contain keys and values that also satisfy the following:
\n
’.Normally, when objects are saved to Parse, they are automatically assigned a unique identifier through the objectId
field, as well as a createdAt
field and updatedAt
field which represent the time that the object was created and last modified in your Parse Server. These fields can be manually set when data is imported from a JSON file. Please keep in mind the following:
objectId
fields.createdAt
field or the updatedAt
field.In addition to the exposed fields, objects in the Parse User class can also have the bcryptPassword
field set. The value of this field is a String
that is the bcrypt hashed password + salt in the modular crypt format described in this StackOverflow answer. Most OpenSSL based bcrypt implementations should have built-in methods to produce these strings.
A file containing a User
object could look like:
{ "results":
[{
"username": "cooldude",
"createdAt": "1983-09-13T22:42:30.548Z",
"updatedAt": "2015-09-04T10:12:42.137Z",
"objectId": "ttttSEpfXm",
"sessionToken": "dfwfq3dh0zwe5y2sqv514p4ib",
"bcryptPassword": "$2a$10$ICV5UeEf3lICfnE9W9pN9.O9Ved/ozNo7G83Qbdk5rmyvY8l16MIK"
}]
}
Note that in CSV the import field types are limited to String
, Boolean
, and Number
.
You can request an export of your data at any time from your app’s Settings page. The data export runs at a lower priority than production queries, so if your app is still serving queries, production traffic will always be given a higher priority, which may slow down the delivery of your data export.
Each collection will be exported in the same JSON format used by our REST API and delivered in a single zipped file. Since data is stored internally as JSON, this allows us to ensure that the export closely matches how the data is saved to Parse. Other formats such as CSV cannot represent all of the data types supported by Parse without losing information. If you’d like to work with your data in CSV format, you can use any of the JSON-to-CSV converters available widely on the web.
For offline analysis of your data, we highly recommend using alternate ways to access your data that do not require extracting the entire collection at once. For example, you can try exporting only the data that has changed since your last export. Here are some ways of achieving this:
Use the JavaScript SDK in a node app. Parse.Query.each()
will allow you to extract every single object that matches a query. You can use date constraints to make sure the query only matches data that has been updated since you last ran this app. Your node app can write this data to disk for offline analysis.
Use the REST API in a script. You can run queries against your class and use skip/limit to page through results, which can then be written to disk for offline analysis. You can again use date constraints to make sure only newly updated data is extracted.
If the above two options do not fit your needs, you can try using the Data Browser to export data selectively. Use the Funnel icon to create a filter for the specific data that you need to export, such as newly updated objects. Once the filter has been applied, click on the Export data icon on the upper right of your Data Browser. This type of export will only include the objects that match your criteria.
There are three kinds of relationships. One-to-one relationships enable one object to be associated with another object. One-to-many relationships enable one object to have many related objects. Finally, many-to-many relationships enable complex relationships among many objects.
There are four ways to build relationships in Parse:
When you’re thinking about one-to-many relationships and whether to implement Pointers or Arrays, there are several factors to consider. First, how many objects are involved in this relationship? If the “many” side of the relationship could contain a very large number (greater than 100 or so) of objects, then you have to use Pointers. If the number of objects is small (fewer than 100 or so), then Arrays may be more convenient, especially if you typically need to get all of the related objects (the “many” in the “one-to-many relationship”) at the same time as the parent object.
Let’s say we have a game app. The game keeps track of the player’s score and achievements every time she chooses to play. In Parse, we can store this data in a single Game
object. If the game becomes incredibly successful, each player will store thousands of Game
objects in the system. For circumstances like this, where the number of relationships can be arbitrarily large, Pointers are the best option.
Suppose in this game app, we want to make sure that every Game
object is associated with a Parse User. We can implement this like so:
PFObject *game= [PFObject objectWithClassName:@"Game"];
[game setObject:[PFUser currentUser] forKey:@"createdBy"];
let game = PFObject(className:"Game")
game["createdBy"] = PFUser.currentUser()
We can obtain all of the Game
objects created by a Parse User with a query:
PFQuery *gameQuery = [PFQuery queryWithClassName:@"Game"];
[gameQuery whereKey:@"createdBy" equalTo:[PFUser currentUser]];
let gameQuery = PFQuery(className:"Game")
if let user = PFUser.currentUser() {
gameQuery.whereKey("createdBy", equalTo: user)
}
And, if we want to find the Parse User who created a specific Game
, that is a lookup on the createdBy
key:
// say we have a Game object
PFObject *game = ...
// getting the user who created the Game
PFUser *createdBy = [game objectForKey@"createdBy"];
// say we have a Game object
let game = ...
// getting the user who created the Game
let createdBy = game["createdBy"]
For most scenarios, Pointers will be your best bet for implementing one-to-many relationships.
Arrays are ideal when we know that the number of objects involved in our one-to-many relationship are going to be small. Arrays will also provide some productivity benefit via the includeKey
parameter. Supplying the parameter will enable you to obtain all of the “many” objects in the “one-to-many” relationship at the same time that you obtain the “one” object. However, the response time will be slower if the number of objects involved in the relationship turns out to be large.
Suppose in our game, we enabled players to keep track of all the weapons their character has accumulated as they play, and there can only be a dozen or so weapons. In this example, we know that the number of weapons is not going to be very large. We also want to enable the player to specify the order in which the weapons will appear on screen. Arrays are ideal here because the size of the array is going to be small and because we also want to preserve the order the user has set each time they play the game:
Let’s start by creating a column on our Parse User object called weaponsList
.
Now let’s store some Weapon
objects in the weaponsList
:
// let's say we have four weapons
PFObject *scimitar = ...
PFObject *plasmaRifle = ...
PFObject *grenade = ...
PFObject *bunnyRabbit = ...
// stick the objects in an array
NSArray *weapons = @[scimitar, plasmaRifle, grenade, bunnyRabbit];
// store the weapons for the user
[[PFUser currentUser] setObject:weapons forKey:@weaponsList"];
// let's say we have four weapons
let scimitar = ...
let plasmaRifle = ...
let grenade = ...
let bunnyRabbit = ...
// stick the objects in an array
let weapons = [scimitar, plasmaRifle, grenade, bunnyRabbit]
// store the weapons for the user
let user = PFUser.currentUser()
user["weaponsList"] = weapons
Later, if we want to retrieve the Weapon
objects, it’s just one line of code:
NSArray *weapons = [[PFUser currentUser] objectForKey:@"weaponsList"];
let weapons = PFUser.currentUser()?.objectForKey("weaponsList")
Sometimes, we will want to fetch the “many” objects in our one-to-many relationship at the same time as we fetch the “one” object. One trick we could employ is to use the includeKey
(or include
in Android) parameter whenever we use a Parse Query to also fetch the array of Weapon
objects (stored in the weaponsList
column) along with the Parse User object:
// set up our query for a User object
PFQuery *userQuery = [PFUser query];
// configure any constraints on your query...
// for example, you may want users who are also playing with or against you
// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
[userQuery includeKey:@"weaponsList"];
// execute the query
[userQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
// objects contains all of the User objects, and their associated Weapon objects, too
}];
// set up our query for a User object
let userQuery = PFUser.query();
// configure any constraints on your query...
// for example, you may want users who are also playing with or against you
// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
userQuery?.includeKey("weaponsList");
// execute the query
userQuery?.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
// objects contains all of the User objects, and their associated Weapon objects, too
}
You can also get the “one” side of the one-to-many relationship from the “many” side. For example, if we want to find all Parse User objects who also have a given Weapon
, we can write a constraint for our query like this:
// add a constraint to query for whenever a specific Weapon is in an array
[userQuery whereKey:@"weaponsList" equalTo:scimitar];
// or query using an array of Weapon objects...
[userQuery whereKey:@"weaponsList" containedIn:arrayOfWeapons];
// add a constraint to query for whenever a specific Weapon is in an array
userQuery?.whereKey("weaponsList", equalTo: scimitar);
// or query using an array of Weapon objects...
userQuery?.whereKey("weaponsList", containedIn: arrayOfWeapons)
Now let’s tackle many-to-many relationships. Suppose we had a book reading app and we wanted to model Book
objects and Author
objects. As we know, a given author can write many books, and a given book can have multiple authors. This is a many-to-many relationship scenario where you have to choose between Arrays, Parse Relations, or creating your own Join Table.
The decision point here is whether you want to attach any metadata to the relationship between two entities. If you don’t, Parse Relation or using Arrays are going to be the easiest alternatives. In general, using arrays will lead to higher performance and require fewer queries. If either side of the many-to-many relationship could lead to an array with more than 100 or so objects, then, for the same reason Pointers were better for one-to-many relationships, Parse Relation or Join Tables will be better alternatives.
On the other hand, if you want to attach metadata to the relationship, then create a separate table (the “Join Table”) to house both ends of the relationship. Remember, this is information about the relationship, not about the objects on either side of the relationship. Some examples of metadata you may be interested in, which would necessitate a Join Table approach, include:
Using Parse Relations, we can create a relationship between a Book
and a few Author
objects. In the Data Browser, you can create a column on the Book
object of type relation and name it authors
.
After that, we can associate a few authors with this book:
// let’s say we have a few objects representing Author objects
PFObject *authorOne = …
PFObject *authorTwo = …
PFObject *authorThree = …
// now we create a book object
PFObject *book= [PFObject objectWithClassName:@"Book"];
// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
PFRelation *relation = [book relationForKey:@"authors"];
[relation addObject:authorOne];
[relation addObject:authorTwo];
[relation addObject:authorThree];
// now save the book object
[book saveInBackground];
// let’s say we have a few objects representing Author objects
let authorOne = ...
let authorTwo = ...
let authorThree = ...
// now we create a book object
let book = PFObject(className: "Book")
// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
let relation = book.relationForKey("authors")
relation.addObject(authorOne)
relation.addObject(authorTwo)
relation.addObject(authorThree)
// now save the book object
book.saveInBackground()
To get the list of authors who wrote a book, create a query:
// suppose we have a book object
PFObject *book = ...
// create a relation based on the authors key
PFRelation *relation = [book relationForKey:@"authors"];
// generate a query based on that relation
PFQuery *query = [relation query];
// now execute the query
// suppose we have a book object
let book = ...
// create a relation based on the authors key
let relation = book.relationForKey("authors")
// generate a query based on that relation
let query = relation.query()
// now execute the query
Perhaps you even want to get a list of all the books to which an author contributed. You can create a slightly different kind of query to get the inverse of the relationship:
// suppose we have a author object, for which we want to get all books
PFObject *author = ...
// first we will create a query on the Book object
PFQuery *query = [PFQuery queryWithClassName:@"Book"];
// now we will query the authors relation to see if the author object
// we have is contained therein
[query whereKey:@"authors" equalTo:author];
// suppose we have a author object, for which we want to get all books
let author = ...
// first we will create a query on the Book object
let query = PFQuery(className: "Book")
// now we will query the authors relation to see if the author object
// we have is contained therein
query?.whereKey("authors", equalTo: author)
There may be certain cases where we want to know more about a relationship. For example, suppose we were modeling a following/follower relationship between users: a given user can follow another user, much as they would in popular social networks. In our app, we not only want to know if User A is following User B, but we also want to know when User A started following User B. This information could not be contained in a Parse Relation. In order to keep track of this data, you must create a separate table in which the relationship is tracked. This table, which we will call Follow
, would have a from
column and a to
column, each with a pointer to a Parse User. Alongside the relationship, you can also add a column with a Date
object named date
.
Now, when you want to save the following relationship between two users, create a row in the Follow
table, filling in the from
, to
, and date
keys appropriately:
// suppose we have a user we want to follow
PFUser *otherUser = ...
// create an entry in the Follow table
PFObject *follow = [PFObject objectWithClassName:@"Follow"];
[follow setObject:[PFUser currentUser] forKey:@"from"];
[follow setObject:otherUser forKey:@"to"];
[follow setObject:[NSDate date] forKey@"date"];
[follow saveInBackground];
// suppose we have a user we want to follow
let otherUser = ...
// create an entry in the Follow table
let follow = PFObject(className: "Follow")
follow.setObject(PFUser.currentUser()!, forKey: "from")
follow.setObject(otherUser, forKey: "to")
follow.setObject(NSDate(), forKey: "date")
follow.saveInBackground()
If we want to find all of the people we are following, we can execute a query on the Follow
table:
// set up the query on the Follow table
PFQuery *query = [PFQuery queryWithClassName:@"Follow"];
[query whereKey:@"from" equalTo:[PFUser currentUser]];
// execute the query
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
for(PFObject *o in objects) {
// o is an entry in the Follow table
// to get the user, we get the object with the to key
PFUser *otherUser = [o objectForKey@"to"];
// to get the time when we followed this user, get the date key
PFObject *when = [o objectForKey@"date"];
}
}];
// set up the query on the Follow table
let query = PFQuery(className: "Follow")
query.whereKey("from", equalTo: PFUser.currentUser()!)
// execute the query
query.findObjectsInBackgroundWithBlock{
(objects: [AnyObject]?, error: NSError?) -> Void in
if let objects = objects {
for o in objects {
// o is an entry in the Follow table
// to get the user, we get the object with the to key
let otherUse = o.objectForKey("to") as? PFUser
// to get the time when we followed this user, get the date key
let when = o.objectForKey("date") as? PFObject
}
}
}
It’s also pretty easy to find all the users that are following the current user by querying on the to
key:
// set up the query on the Follow table
PFQuery *query = [PFQuery queryWithClassName:@"Follow"];
[query whereKey:@"to" equalTo:[PFUser currentUser]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
for(PFObject *o in objects) {
// o is an entry in the Follow table
// to get the user, we get the object with the from key
PFUser *otherUser = [o objectForKey@"from"];
// to get the time the user was followed, get the date key
PFObject *when = [o objectForKey@"date"];
}
}];
// set up the query on the Follow table
let query = PFQuery(className: "Follow")
query.whereKey("to", equalTo: PFUser.currentUser()!)
query.findObjectsInBackgroundWithBlock{
(objects: [AnyObject]?, error: NSError?) -> Void in
if let objects = objects {
for o in objects {
// o is an entry in the Follow table
// to get the user, we get the object with the to key
let otherUse = o.objectForKey("to") as? PFUser
// to get the time when we followed this user, get the date key
let when = o.objectForKey("date") as? PFObject
}
}
}
Arrays are used in Many-to-Many relationships in much the same way that they are for One-to-Many relationships. All objects on one side of the relationship will have an Array column containing several objects on the other side of the relationship.
Suppose we have a book reading app with Book
and Author
objects. The Book
object will contain an Array of Author
objects (with a key named authors
). Arrays are a great fit for this scenario because it’s highly unlikely that a book will have more than 100 or so authors. We will put the Array in the Book
object for this reason. After all, an author could write more than 100 books.
Here is how we save a relationship between a Book
and an Author
.
// let's say we have an author
PFObject *author = ...
// and let's also say we have an book
PFObject *book = ...
// add the author to the authors list for the book
[book addObject:author forKey:@"authors"];
// let's say we have an author
let author = ...
// and let's also say we have an book
let book = ...
// add the author to the authors list for the book
book.addObject(author, forKey: "authors")
Because the author list is an Array, you should use the includeKey
(or include
on Android) parameter when fetching a Book
so that Parse returns all the authors when it also returns the book:
// set up our query for the Book object
PFQuery *bookQuery = [PFQuery queryWithClassName:@"Book"];
// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
[bookQuery includeKey:@"authors"];
// execute the query
[bookQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
// objects is all of the Book objects, and their associated
// Author objects, too
}];
// set up our query for the Book object
let bookQuery = PFQuery(className: "Book")
// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
bookQuery.includeKey("authors")
// execute the query
bookQuery.findObjectsInBackgroundWithBlock{
(objects: [AnyObject]?, error: NSError?) -> Void in
// objects is all of the Book objects, and their associated
// Author objects, too
}
At that point, getting all the Author
objects in a given Book
is a pretty straightforward call:
NSArray *authorList = [book objectForKey@"authors"];
let authorList = book.objectForKey("authors") as? NSArray
Finally, suppose you have an Author
and you want to find all the Book
objects in which she appears. This is also a pretty straightforward query with an associated constraint:
// suppose we have an Author object
PFObject *author = ...
// set up our query for the Book object
PFQuery *bookQuery = [PFQuery queryWithClassName:@"Book"];
// configure any constraints on your query...
[bookQuery whereKey:@"authors" equalTo:author];
// tell the query to fetch all of the Author objects along with the Book
[bookQuery includeKey:@"authors"];
// execute the query
[bookQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
// objects is all of the Book objects, and their associated Author objects, too
}];
// suppose we have an Author object
let author = ...
// set up our query for the Book object
let bookQuery = PFQuery(className: "Book")
// configure any constraints on your query...
bookQuery.whereKey("authors", equalTo: author)
// tell the query to fetch all of the Author objects along with the Book
bookQuery.includeKey("authors")
// execute the query
bookQuery.findObjectsInBackgroundWithBlock{
(objects: [AnyObject]?, error: NSError?) -> Void in
// objects is all of the Book objects, and their associated Author objects, too
}
In Parse, a one-to-one relationship is great for situations where you need to split one object into two objects. These situations should be rare, but two examples include:
Thank you for reading this far. We apologize for the complexity. Modeling relationships in data is a hard subject, in general. But look on the bright side: it’s still easier than relationships with people.
Parse has a few simple patterns for surfacing errors and handling them in your code.
There are two types of errors you may encounter. The first is those dealing with logic errors in the way you’re using the SDK. These types of errors result in an NSException
being raised. For an example take a look at the following code:
PFUser *user = [PFUser user];
[user signUp];
let user = PFUser()
user.signUp
This will throw an NSInternalInconsistencyException
because signUp
was called without first setting the required properties (username
and password
).
The second type of error is one that occurs when interacting with Parse Server over the network. These errors are either related to problems connecting to the cloud or problems performing the requested operation. Let’s take a look at another example:
- (void)getMyNote {
PFQuery *query = [PFQuery queryWithClassName:@"Note"];
[query getObjectInBackgroundWithId:@"thisObjectIdDoesntExist"
target:self
selector:@selector(callbackForGet:error:)];
}
func getMyNote() -> Void {
let query = PFQuery(className: "Note")
query.getObjectInBackgroundWithId("thisObjectIdDoesntExist", target: self, selector: Selector("callbackForGet:error:"))
}
In the above code, we try to fetch an object with a non-existent objectId
. Parse Server will return an error with an error code set in code
and message in the error’s userInfo
. Here’s how to handle it properly in your callback:
- (void)callbackForGet:(PFObject *)result error:(NSError *)error {
if (result) {
NSLog(@"Everything went fine!");
} else {
if ([error code] == kPFErrorObjectNotFound) {
NSLog(@"Uh oh, we couldn't find the object!");
} else if (error) {
NSLog(@"Error: %@", [error userInfo][@"error"]);
}
}
}
func callbackForGet(result: PFObject?, error: NSError?) -> Void {
if let result = result {
print("Everything went fine!")
} else {
if let error = error {
if error._code == PFErrorCode.errorObjectNotFound.rawValue {
print("Uh oh, we couldn't find the object!")
} else {
let errorString = error._userInfo!["error"] as? NSString
print("Error: \(errorString)")
}
}
}
}
The query might also fail because the device couldn’t connect to your Parse Server. Here’s the same callback but with a bit of extra code to handle that scenario explicitly:
- (void)callbackForGet:(PFObject *)result error:(NSError *)error {
if (result) {
NSLog(@"Everything went fine!");
} else {
if ([error code] == kPFErrorObjectNotFound) {
NSLog(@"Uh oh, we couldn't find the object!");
// Now also check for connection errors:
} else if ([error code] == kPFErrorConnectionFailed) {
NSLog(@"Uh oh, we couldn't even connect to the Parse Cloud!");
} else if (error) {
NSLog(@"Error: %@", [error userInfo][@"error"]);
}
}
}
func callbackForGet(result: PFObject?, error: NSError?) -> Void {
if let result = result {
print("Everything went fine!")
} else {
if let error = error {
if error._code == PFErrorCode.errorObjectNotFound.rawValue {
print("Uh oh, we couldn't find the object!")
// Now also check for connection errors:
} else if error._code == PFErrorCode.errorConnectionFailed.rawValue {
print("Uh oh, we couldn't even connect to the Parse Cloud!")
} else {
let errorString = error._userInfo!["error"] as? NSString
print("Error: \(errorString)")
}
}
}
}
When the callback expects a NSNumber
, its boolValue
tells you whether the operation succeeded or not. For example, this is how you might implement the callback for PFObject
’s saveInBackgroundWithTarget:selector:
method:
- (void)callbackForSave:(NSNumber *)result error:(NSError *)error {
if ([result boolValue]) {
NSLog(@"Everything went fine!");
} else {
if ([error code] == kPFErrorConnectionFailed) {
NSLog(@"Uh oh, we couldn't even connect to the Parse Cloud!");
} else if (error) {
NSLog(@"Error: %@", [error userInfo][@"error"]);
}
}
}
func callbackForSave(result: NSNumber?, error: NSError?) -> Void {
if result?.boolValue == true {
print("Everything went fine!")
} else {
if let error = error {
if error.code == PFErrorCode.ErrorConnectionFailed.rawValue {
print("Uh oh, we couldn't even connect to the Parse Cloud!")
} else {
let errorString = error.userInfo!["error"] as? NSString
print("Error: \(errorString)")
}
}
}
}
For synchronous (non-background) methods, error handling is mostly the same except that instead of a NSNumber
representing success or failure you’ll get an actual BOOL
directly.
By default, all connections have a timeout of 10 seconds, so the synchronous methods will not hang indefinitely.
For a list of all possible NSError
types, scroll down to Error Codes, or see the PFErrorCode
section of the iOS-OSX API.
As your app development progresses, you will want to use Parse’s security features in order to safeguard data. This document explains the ways in which you can secure your apps.
If your app is compromised, it’s not only you as the developer who suffers, but potentially the users of your app as well. Continue reading for our suggestions for sensible defaults and precautions to take before releasing your app into the wild.
When an app first connects to Parse, it identifies itself with an Application ID and a Client key (or REST Key, or .NET Key, or JavaScript Key, depending on which platform you’re using). These are not secret and by themselves they do not secure an app. These keys are shipped as a part of your app, and anyone can decompile your app or proxy network traffic from their device to find your client key. This exploit is even easier with JavaScript — one can simply “view source” in the browser and immediately find your client key.
This is why Parse has many other security features to help you secure your data. The client key is given out to your users, so anything that can be done with just the client key is doable by the general public, even malicious hackers.
The master key, on the other hand, is definitely a security mechanism. Using the master key allows you to bypass all of your app’s security mechanisms, such as class-level permissions and ACLs. Having the master key is like having root access to your app’s servers, and you should guard your master key with the same zeal with which you would guard your production machines’ root password.
The overall philosophy is to limit the power of your clients (using client keys), and to perform any sensitive actions requiring the master key in Cloud Code. You’ll learn how to best wield this power in the section titled Implementing Business Logic in Cloud Code.
A final note: It is recommended to setup HTTPS and SSL in your server, to avoid man-in-the-middle attacks, but Parse works fine as well with non-HTTPS connections.
The second level of security is at the schema and data level. Enforcing security measures at this level will restrict how and when client applications can access and create data on Parse. When you first begin developing your Parse application, all of the defaults are set so that you can be a more productive developer. For example:
You can configure any of these permissions to apply to everyone, no one, or to specific users or roles in your app. Roles are groups that contain users or other roles, which you can assign to an object to restrict its use. Any permission granted to a role is also granted to any of its children, whether they are users or other roles, enabling you to create an access hierarchy for your apps. Each of the Parse guides includes a detailed description of employing Roles in your apps.
Once you are confident that you have the right classes and relationships between classes in your app, you should begin to lock it down by doing the following:
Almost every class that you create should have these permissions tweaked to some degree. For classes where every object has the same permissions, class-level settings will be most effective. For example, one common use case entails having a class of static data that can be read by anyone but written by no one.
As a start, you can configure your application so that clients cannot create new classes on Parse. This is done by setting the key allowClientClassCreation
to false
in your ParseServer configuration. See the project Readme for an overview of Configuring your ParseServer. Once restricted, classes may only be created from the Data Browser or with a the masterKey
. This will prevent attackers from filling your database with unlimited, arbitrary new classes.
Requires Parse Server 5.0.0+
By default, Parse Server creates Users with public read access. This allows other users, and un-authenticated users, to read data such as email
. When moving to production, set the key enforcePrivateUsers
to true
, as this will remove the public read access to new users.
Parse lets you specify what operations are allowed per class. This lets you restrict the ways in which clients can access or modify your classes. To change these settings, go to the Data Browser, select a class, and click the “Security” button.
You can configure the client’s ability to perform each of the following operations for the selected class:
Read:
Get: With Get permission, users can fetch objects in this table if they know their objectIds.
Find: Anyone with Find permission can query all of the objects in the table, even if they don’t know their objectIds. Any table with public Find permission will be completely readable by the public, unless you put an ACL on each object.
Write:
Update: Anyone with Update permission can modify the fields of any object in the table that doesn’t have an ACL. For publicly readable data, such as game levels or assets, you should disable this permission.
Create: Like Update, anyone with Create permission can create new objects of a class. As with the Update permission, you’ll probably want to turn this off for publicly readable data.
Delete: With this permission, people can delete any object in the table that doesn’t have an ACL. All they need is its objectId.
Add fields: Parse classes have schemas that are inferred when objects are created. While you’re developing your app, this is great, because you can add a new field to your object without having to make any changes on the backend. But once you ship your app, it’s very rare to need to add new fields to your classes automatically. You should pretty much always turn off this permission for all of your classes when you submit your app to the public.
For each of the above actions, you can grant permission to all users (which is the default), or lock permissions down to a list of roles and users. For example, a class that should be available to all users would be set to read-only by only enabling get and find. A logging class could be set to write-only by only allowing creates. You could enable moderation of user-generated content by providing update and delete access to a particular set of users or roles.
Once you’ve locked down your schema and class-level permissions, it’s time to think about how data is accessed by your users. Object-level access control enables one user’s data to be kept separate from another’s, because sometimes different objects in a class need to be accessible by different people. For example, a user’s private personal data should be accessible only to them.
Parse also supports the notion of anonymous users for those apps that want to store and protect user-specific data without requiring explicit login.
When a user logs into an app, they initiate a session with Parse. Through this session they can add and modify their own data but are prevented from modifying other users’ data.
The easiest way to control who can access which data is through access control lists, commonly known as ACLs. The idea behind an ACL is that each object has a list of users and roles along with what permissions that user or role has. A user needs read permissions (or must belong to a role that has read permissions) in order to retrieve an object’s data, and a user needs write permissions (or must belong to a role that has write permissions) in order to update or delete that object.
Once you have a User, you can start using ACLs. Remember: Users can be created through traditional username/password sign up, through a third-party login system like Facebook or Twitter, or even by using Parse’s automatic anonymous users functionality. To set an ACL on the current user’s data to not be publicly readable, all you have to do is:
PFUser *user = [PFUser currentUser];
user.ACL = [PFACL ACLWithUser:user];
if let user = PFUser.current() {
user.ACL = PFACL(user: user)
}
Most apps should do this. If you store any sensitive user data, such as email addresses or phone numbers, you need to set an ACL like this so that the user’s private information isn’t visible to other users. If an object doesn’t have an ACL, it’s readable and writeable by everyone. The only exception is the _User
class. We never allow users to write each other’s data, but they can read it by default. (If you as the developer need to update other _User
objects, remember that your master key can provide the power to do this.)
To make it super easy to create user-private ACLs for every object, we have a way to set a default ACL that will be used for every new object you create:
[PFACL setDefaultACL:[PFACL ACL] withAccessForCurrentUser:YES];
PFACL.setDefault(PFACL(), withAccessForCurrentUser: true)
If you want the user to have some data that is public and some that is private, it’s best to have two separate objects. You can add a pointer to the private data from the public one.
PFObject *privateData = [PFObject objectWithClassName:@"PrivateUserData"];
privateData.ACL = [PFACL ACLWithUser:[PFUser currentUser]];
[privateData setObject:@"555-5309" forKey:@"phoneNumber"];
[[PFUser currentUser] setObject:privateData forKey:@"privateData"];
if let currentUser = PFUser.current() {
let privateData = PFObject(className: "PrivateUserData")
privateData.ACL = PFACL(user: currentUser)
privateData.setObject("555-5309", forKey: "phoneNumber")
currentUser.setObject(privateData, forKey: "privateData")
}
Of course, you can set different read and write permissions on an object. For example, this is how you would create an ACL for a public post by a user, where anyone can read it:
PFACL *acl = [PFACL ACL];
[acl setPublicReadAccess:true];
[acl setWriteAccess:true forUser:[PFUser currentUser]];
let acl = PFACL()
acl.hasPublicReadAccess = true
if let currentUser = PFUser.currentUser() {
acl.setWriteAccess(true, for: currentUser)
}
Sometimes it’s inconvenient to manage permissions on a per-user basis, and you want to have groups of users who get treated the same (like a set of admins with special powers). Roles are a special kind of object that let you create a group of users that can all be assigned to the ACL. The best thing about roles is that you can add and remove users from a role without having to update every single object that is restricted to that role. To create an object that is writeable only by admins:
// Assuming you've already created a role called "admins"...
PFACL *acl = [PFACL ACL];
[acl setPublicReadAccess:true];
[acl setWriteAccess:true forRoleWithName:@"admins"];
let acl = PFACL()
acl.setPublicReadAccess(true)
acl.setWriteAccess(true, forRoleWithName: "admins")
Of course, this snippet assumes you’ve already created a role named “admins”. This is often reasonable when you have a small set of special roles set up while developing your app. Roles can also be created and updated on the fly — for example, adding new friends to a “friendOf___” role after each connection is made.
All this is just the beginning. Applications can enforce all sorts of complex access patterns through ACLs and class-level permissions. For example:
For the curious, here’s the format for an ACL that restricts read and write permissions to the owner (whose objectId
is identified by "aSaMpLeUsErId"
) and enables other users to read the object:
{
"*": { "read":true },
"aSaMpLeUsErId": { "read" :true, "write": true }
}
And here’s another example of the format of an ACL that uses a Role:
{
"role:RoleName": { "read": true },
"aSaMpLeUsErId": { "read": true, "write": true }
}
Pointer permissions are a special type of class-level permission that create a virtual ACL on every object in a class, based on users stored in pointer fields on those objects. For example, given a class with an owner
field, setting a read pointer permission on owner
will make each object in the class only readable by the user in that object’s owner
field. For a class with a sender
and a reciever
field, a read pointer permission on the receiver
field and a read and write pointer permission on the sender
field will make each object in the class readable by the user in the sender
and receiver
field, and writable only by the user in the sender
field.
Given that objects often already have pointers to the user(s) that should have permissions on the object, pointer permissions provide a simple and fast solution for securing your app using data which is already there, that doesn’t require writing any client code or cloud code.
Pointer permissions are like virtual ACLs. They don’t appear in the ACL column, but if you are familiar with how ACLs work, you can think of them like ACLs. In the above example with the sender
and receiver
, each object will act as if it has an ACL of:
{
"<SENDER_USER_ID>": {
"read": true,
"write": true
},
"<RECEIVER_USER_ID>": {
"read": true
}
}
Note that this ACL is not actually created on each object. Any existing ACLs will not be modified when you add or remove pointer permissions, and any user attempting to interact with an object can only interact with the object if both the virtual ACL created by the pointer permissions, and the real ACL already on the object allow the interaction. For this reason, it can sometimes be confusing to combine pointer permissions and ACLs, so we recommend using pointer permissions for classes that don’t have many ACLs set. Fortunately, it’s easy to remove pointer permissions if you later decide to use Cloud Code or ACLs to secure your app.
Starting version 2.3.0, parse-server introduces a new Class Level Permission requiresAuthentication
.
This CLP prevents any non authenticated user from performing the action protected by the CLP.
For example, you want to allow your authenticated users to find
and get
Announcement
’s from your application and your admin role to have all privileged, you would set the CLP:
// POST http://my-parse-server.com/schemas/Announcement
// Set the X-Parse-Application-Id and X-Parse-Master-Key header
// body:
{
classLevelPermissions:
{
"find": {
"requiresAuthentication": true,
"role:admin": true
},
"get": {
"requiresAuthentication": true,
"role:admin": true
},
"create": { "role:admin": true },
"update": { "role:admin": true },
"delete": { "role:admin": true }
}
}
Effects:
:warning: Note that this is in no way securing your content, if you allow anyone to login to your server, every client will still be able to query this object.
Class-Level Permissions (CLPs) and Access Control Lists (ACLs) are both powerful tools for securing your app, but they don’t always interact exactly how you might expect. They actually represent two separate layers of security that each request has to pass through to return the correct information or make the intended change. These layers, one at the class level, and one at the object level, are shown below. A request must pass through BOTH layers of checks in order to be authorized. Note that despite acting similarly to ACLs, Pointer Permissions are a type of class level permission, so a request must pass the pointer permission check in order to pass the CLP check.
As you can see, whether a user is authorized to make a request can become complicated when you use both CLPs and ACLs. Let’s look at an example to get a better sense of how CLPs and ACLs can interact. Say we have a Photo
class, with an object, photoObject
. There are 2 users in our app, user1
and user2
. Now lets say we set a Get CLP on the Photo
class, disabling public Get, but allowing user1
to perform Get. Now let’s also set an ACL on photoObject
to allow Read - which includes GET - for only user2
.
You may expect this will allow both user1
and user2
to Get photoObject
, but because the CLP layer of authentication and the ACL layer are both in effect at all times, it actually makes it so neither user1
nor user2
can Get photoObject
. If user1
tries to Get photoObject
, it will get through the CLP layer of authentication, but then will be rejected because it does not pass the ACL layer. In the same way, if user2
tries to Get photoObject
, it will also be rejected at the CLP layer of authentication.
Now lets look at example that uses Pointer Permissions. Say we have a Post
class, with an object, myPost
. There are 2 users in our app, poster
, and viewer
. Lets say we add a pointer permission that gives anyone in the Creator
field of the Post
class read and write access to the object, and for the myPost
object, poster
is the user in that field. There is also an ACL on the object that gives read access to viewer
. You may expect that this will allow poster
to read and edit myPost
, and viewer
to read it, but viewer
will be rejected by the Pointer Permission, and poster
will be rejected by the ACL, so again, neither user will be able to access the object.
Because of the complex interaction between CLPs, Pointer Permissions, and ACLs, we recommend being careful when using them together. Often it can be useful to use CLPs only to disable all permissions for a certain request type, and then using Pointer Permissions or ACLs for other request types. For example, you may want to disable Delete for a Photo
class, but then put a Pointer Permission on Photo
so the user who created it can edit it, just not delete it. Because of the especially complex way that Pointer Permissions and ACLs interact, we usually recommend only using one of those two types of security mechanisms.
There are some special classes in Parse that don’t follow all of the same security rules as every other class. Not all classes follow Class-Level Permissions (CLPs) or Access Control Lists (ACLs) exactly how they are defined, and here those exceptions are documented. Here “normal behavior” refers to CLPs and ACLs working normally, while any other special behaviors are described in the footnotes.
_User |
_Installation |
|
---|---|---|
Get | normal behaviour [1, 2, 3] | ignores CLP, but not ACL |
Find | normal behavior [3] | master key only [6] |
Create | normal behavior [4] | ignores CLP |
Update | normal behavior [5] | ignores CLP, but not ACL [7] |
Delete | normal behavior [5] | master key only [7] |
Add Field | normal behavior | normal behavior |
Logging in, or /parse/login
in the REST API, does not respect the Get CLP on the user class. Login works just based on username and password, and cannot be disabled using CLPs.
Retrieving the current user, or becoming a User based on a session token, which are both /parse/users/me
in the REST API, do not respect the Get CLP on the user class.
Read ACLs do not apply to the logged in user. For example, if all users have ACLs with Read disabled, then doing a find query over users will still return the logged in user. However, if the Find CLP is disabled, then trying to perform a find on users will still return an error.
Create CLPs also apply to signing up. So disabling Create CLPs on the user class also disables people from signing up without the master key.
Users can only Update and Delete themselves. Public CLPs for Update and Delete may still apply. For example, if you disable public Update for the user class, then users cannot edit themselves. But no matter what the write ACL on a user is, that user can still Update or Delete itself, and no other user can Update or Delete that user. As always, however, using the master key allows users to update other users, independent of CLPs or ACLs.
Get requests on installations follow ACLs normally. Find requests without master key is not allowed unless you supply the installationId
as a constraint.
Update requests on installations do adhere to the ACL defined on the installation, but Delete requests are master-key-only. For more information about how installations work, check out the installations section of the REST guide.
For most apps, care around keys, class-level permissions, and object-level ACLs are all you need to keep your app and your users’ data safe. Sometimes, though, you’ll run into an edge case where they aren’t quite enough. For everything else, there’s Cloud Code.
Cloud Code allows you to upload JavaScript to Parse’s servers, where we will run it for you. Unlike client code running on users’ devices that may have been tampered with, Cloud Code is guaranteed to be the code that you’ve written, so it can be trusted with more responsibility.
One particularly common use case for Cloud Code is preventing invalid data from being stored. For this sort of situation, it’s particularly important that a malicious client not be able to bypass the validation logic.
To create validation functions, Cloud Code allows you to implement a beforeSave
trigger for your class. These triggers are run whenever an object is saved, and allow you to modify the object or completely reject a save. For example, this is how you create a Cloud Code beforeSave trigger to make sure every user has an email address set:
Parse.Cloud.beforeSave(Parse.User, request => {
const user = request.object;
if (!user.get("email")) {
throw "Every user must have an email address.";
}
});
Validations can lock down your app so that only certain values are acceptable. You can also use afterSave
validations to normalize your data (e.g. formatting all phone numbers or currency identically). You get to retain most of the productivity benefits of accessing Parse data directly from your client applications, but you can also enforce certain invariants for your data on the fly.
Common scenarios that warrant validation include:
While validation often makes sense in Cloud Code, there are likely certain actions that are particularly sensitive, and should be as carefully guarded as possible. In these cases, you can remove permissions or the logic from clients entirely and instead funnel all such operations to Cloud Code functions.
When a Cloud Code function is called, it can use the optional {useMasterKey:true}
parameter to gain the ability to modify user data. With the master key, your Cloud Code function can override any ACLs and write data. This means that it’ll bypass all the security mechanisms you’ve put in place in the previous sections.
Say you want to allow a user to “like” a Post
object without giving them full write permissions on the object. You can do this by having the client call a Cloud Code function instead of modifying the Post itself:
The master key should be used carefully. setting useMasterKey
to true
only in the individual API function calls that need that security override:
Parse.Cloud.define("like", async request => {
var post = new Parse.Object("Post");
post.id = request.params.postId;
post.increment("likes");
await post.save(null, { useMasterKey: true })
});
One very common use case for Cloud Code is sending push notifications to particular users. In general, clients can’t be trusted to send push notifications directly, because they could modify the alert text, or push to people they shouldn’t be able to. Your app’s settings will allow you to set whether “client push” is enabled or not; we recommend that you make sure it’s disabled. Instead, you should write Cloud Code functions that validate the data to be pushed and sent before sending a push.
It’s important to restrict how often a client can call the Parse Server API. This prevents malicious attacks that could:
Parse Sever offers a mechanism to enforce rate limits by setting the Parse Server option rateLimit
, or by specifying a rateLimit
object on a Cloud Function validator.
The valid options for a rate limit are:
requestPath
: The path of the API route to be rate limited.requestMethods
: Optional, the HTTP request methods to be rate limited.requestTimeWindow
: The window of time in milliseconds within which the number of requests set in requestCount
can be made before the rate limit is applied.requestCount
: The number of requests that can be made per IP address within the time window set in requestTimeWindow
before the rate limit is applied.errorResponseMessage
: The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is Too many requests.
.includeInternalRequests
: Optional, whether the rate limit will also apply to requests that are made in by Cloud Code.includeMasterKey
: Optional, whether the rate limit will also apply to requests using the masterKey
redisUrl
Optional, the URL of the Redis server to store rate limit data.To specify a server-wide rate limit of 200 requests per 15 minute window:
const parseServer = new ParseServer({
rateLimit: {
requestPath: '*',
requestTimeWindow: 15 * 60 * 1000,
requestCount: 200,
},
});
To specify a cloud function specific rate limit of 3 request per hour:
Parse.Cloud.define('someFunction', () => {
return 'Hello world';
}, {
rateLimit: {
requestTimeWindow: 60 * 60 * 1000,
requestCount: 3,
}
});
Rate limits can also be applied to beforeSave
triggers to restrict how often a given class is written to:
Parse.Cloud.beforeSave('TestObject', () => {}, {
rateLimit: {
requestTimeWindow: 1 * 60 * 1000 // one write per minute,,
requestCount: 1,
errorResponseMessage: 'Too many requests!',
},
});
⚠️ Rate limits should be enforced as far away from Parse Server as possible to mitigate possible impacts on resource costs, availability and integrity. While Parse Server offers a rate limiting mechanism as a conveniently available security feature without requiring a deep level of expertise, it is not considered best practice to enforce rate limits only after requests already reached the server. For better protection we advice to examine your network architecture an consider enforcing rate limits on the outer edge of the cloud if using a content delivery network, or at least before requests reach the server resource. Consult your cloud service provider for recommended rate limit and firewall solutions for your resources.
Parse provides a number of ways for you to secure data in your app. As you build your app and evaluate the kinds of data you will be storing, you can make the decision about which implementation to choose.
It is worth repeating that that the Parse User object is readable by all other users by default. You will want to set the ACL on your User object accordingly if you wish to prevent data contained in the User object (for example, the user’s email address) from being visible by other users.
Most classes in your app will fall into one of a couple of easy-to-secure categories. For fully public data, you can use class-level permissions to lock down the table to put publicly readable and writeable by no one. For fully private data, you can use ACLs to make sure that only the user who owns the data can read it. But occasionally, you’ll run into situations where you don’t want data that’s fully public or fully private. For example, you may have a social app, where you have data for a user that should be readable only to friends whom they’ve approved. For this you’ll need to a combination of the techniques discussed in this guide to enable exactly the sharing rules you desire.
We hope that you’ll use these tools to do everything you can to keep your app’s data and your users’ data secure. Together, we can make the web a safer place.
As your app scales, you will want to ensure that it performs well under increased load and usage. This document provides guidelines on how you can optimize your app’s performance. While you can use Parse Server for quick prototyping and not worry about performance, you will want to keep our performance guidelines in mind when you’re initially designing your app. We strongly advise that you make sure you’ve followed all suggestions before releasing your app.
You can improve your app’s performance by looking at the following:
Keep in mind that not all suggestions may apply to your app. Let’s look into each one of these in more detail.
Parse objects are stored in a database. A Parse query retrieves objects that you are interested in based on conditions you apply to the query. To avoid looking through all the data present in a particular Parse class for every query, the database can use an index. An index is a sorted list of items matching a given criteria. Indexes help because they allow the database to do an efficient search and return matching results without looking at all of the data. Indexes are typically smaller in size and available in memory, resulting in faster lookups.
You are responsible for managing your database and maintaining indexes when using Parse Server. If your data is not indexed, every query will have to go through the the entire data for a class to return a query result. On the other hand, if your data is indexed appropriately, the number of documents scanned to return a correct query result should be low.
The order of a query constraint’s usefulness is:
Take a look at the following query to retrieve GameScore objects:
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"score" equalTo:@50];
[query whereKey:@"playerName"
containedIn:@[@"Jonathan Walsh", @"Dario Wunsch", @"Shawn Simon"]];
let query = PFQuery.queryWithClassName("GameScore")
query.whereKey("score", equalTo: 50)
query.whereKey("playerName", containedIn: ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"])
Creating an index query based on the score field would yield a smaller search space in general than creating one on the playerName
field.
When examining data types, booleans have a very low entropy and and do not make good indexes. Take the following query constraint:
[query whereKey:@"cheatMode" equalTo:@NO];
query.whereKey("cheatMode", equalTo: false)
The two possible values for "cheatMode"
are true
and false
. If an index was added on this field it would be of little use because it’s likely that 50% of the records will have to be looked at to return query results.
Data types are ranked by their expected entropy of the value space for the key:
Even the best indexing strategy can be defeated by suboptimal queries.
Writing efficient queries means taking full advantage of indexes. Let’s take a look at some query constraints that negate the use of indexes:
Additionally, the following queries under certain scenarios may result in slow query responses if they can’t take advantage of indexes:
For example, let’s say you’re tracking high scores for a game in a GameScore class. Now say you want to retrieve the scores for all players except a certain one. You could create this query:
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerName" notEqualTo:@"Michael Yabuti"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// Retrieved scores successfully
}
}];
let query = PFQuery.queryWithClassName("GameScore")
query.whereKey("playerName", notEqualTo: "Michael Yabuti")
query.findObjectsInBackgroundWithBlock {
(objects, error) in
if !error {
// Retrieved scores successfully
}
}
This query can’t take advantage of indexes. The database has to look at all the objects in the "GameScore"
class to satisfy the constraint and retrieve the results. As the number of entries in the class grows, the query takes longer to run.
Luckily, most of the time a “Not Equal To” query condition can be rewritten as a “Contained In” condition. Instead of querying for the absence of values, you ask for values which match the rest of the column values. Doing this allows the database to use an index and your queries will be faster.
For example if the User class has a column called state which has values “SignedUp”, “Verified”, and “Invited”, the slow way to find all users who have used the app at least once would be to run the query:
PFQuery *query = [PFUser query];
[query whereKey:@"state" notEqualTo:@"Invited"];
var query = PFUser.query()
query.whereKey("state", notEqualTo: "Invited")
It would be faster to use the “Contained In” condition when setting up the query:
[query whereKey:@"state"
containedIn:@[@"SignedUp", @"Verified"]];
query.whereKey("state", containedIn: ["SignedUp", "Verified"])
Sometimes, you may have to completely rewrite your query. Going back to the "GameScore"
example, let’s say we were running that query to display players who had scored higher than the given player. We could do this differently, by first getting the given player’s high score and then using the following query:
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
// Previously retrieved highScore for Michael Yabuti
[query whereKey:@"score" greaterThan:highScore];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// Retrieved scores successfully
}
}];
let query = PFQuery.queryWithClassName("GameScore")
// Previously retrieved highScore for Michael Yabuti
query.whereKey("score", greaterThan: highScore)
query.findObjectsInBackgroundWithBlock {
(objects, error) in
if !error {
// Retrieved scores successfully
}
}
The new query you use depends on your use case. This may sometimes mean a redesign of your data model.
Similar to “Not Equal To”, the “Not Contained In” query constraint can’t use an index. You should try and use the complementary “Contained In” constraint. Building on the User example, if the state column had one more value, “Blocked”, to represent blocked users, a slow query to find active users would be:
PFQuery *query = [PFUser query];
[query whereKey:@"state" notContainedIn:@[@"Invited", @"Blocked"]];
var query = PFUser.query()
query.whereKey("state", notContainedIn: ["Invited", "Blocked"])
Using a complimentary “Contained In” query constraint will always be faster:
[query whereKey:@"state" containedIn:@[@"SignedUp", @"Verified"]];
query.whereKey("state", containedIn: ["SignedUp", "Verified"])
This means rewriting your queries accordingly. Your query rewrites will depend on your schema set up. It may mean redoing that schema.
Regular expression queries should be avoided due to performance considerations. MongoDB is not efficient for doing partial string matching except for the special case where you only want a prefix match. Queries that have regular expression constraints are therefore very expensive, especially for classes with over 100,000 records. Consider restricting how many such operations can be run on a particular app at any given time.
You should avoid using regular expression constraints that don’t use indexes. For example, the following query looks for data with a given string in the "playerName"
field. The string search is case insensitive and therefore cannot be indexed:
[query whereKey:@"playerName" matchesRegex:@"Michael" modifiers:@"i"];
query.whereKey("playerName", matchesRegex: "Michael", modifiers: "i")
The following query, while case sensitive, looks for any occurrence of the string in the field and cannot be indexed:
[query whereKey:@"playerName" containsString:@"Michael"];
query.whereKey("playerName", containsString: "Michael")
These queries are both slow. In fact, the matches
and contains
query constraints are not covered in our querying guides on purpose and we do not recommend using them. Depending on your use case, you should switch to using the following constraint that uses an index, such as:
[query whereKey:@"playerName" hasPrefix:@"Michael"];
query.whereKey("playerName", hasPrefix: "Michael")
This looks for data that starts with the given string. This query will use the backend index, so it will be faster even for large datasets.
As a best practice, when you use regular expression constraints, you’ll want to ensure that other constraints in the query reduce the result set to the order of hundreds of objects to make the query efficient. If you must use the matches
or contains
constraints for legacy reasons, then use case sensitive, anchored queries where possible, for example:
[query whereKey:@"playerName" matchesRegex:@"^Michael"];
query.whereKey("playerName", matchesRegex: "^Michael")
Most of the use cases around using regular expressions involve implementing search. A more performant way of implementing search is detailed later.
Writing restrictive queries allows you to return only the data that the client needs. This is critical in a mobile environment were data usage can be limited and network connectivity unreliable. You also want your mobile app to appear responsive and this is directly affected by the objects you send back to the client. The Querying section shows the types of constraints you can add to your existing queries to limit the data returned. When adding constraints, you want to pay attention and design efficient queries.
You can use skip and limit to page through results and load the data as is needed. The query limit is 100 by default:
query.limit = 10; // limit to at most 10 results
query.limit = 10 // limit to at most 10 results
If you’re issuing queries on GeoPoints, make sure you specify a reasonable radius:
PFQuery *query = [PFQuery queryWithClassName:@"Place"];
[query whereKey:@"location" nearGeoPoint:userGeoPoint withinMiles:10.0];
[query findObjectsInBackgroundWithBlock:^(NSArray *places, NSError *error) {
if (!error) {
// List of objects within 10 miles of a user's location
}
}];
let query = PFQuery.queryWithClassName("Place")
query.whereKey("location", nearGeoPoint: userGeoPoint, withinMiles: 10.0)
query.findObjectsInBackgroundWithBlock {
(places, error) in
if !error {
// List of places within 10 miles of a user's location
}
}
You can further limit the fields returned by calling select:
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query selectKeys:@[@"score", @"playerName"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// each of results will only have the selected fields available.
}
}];
let query = PFQuery.queryWithClassName("GameScore")
query.selectKeys(["score", "playerName"])
query.findObjectsInBackgroundWithBlock {
(objects, error) in
if !error {
// each of results will only have the selected fields available.
}
}
For queries run from iOS and Android, you can turn on query caching. See the iOS and Android guides for more details. Caching queries will increase your mobile app’s performance especially in cases where you want to display cached data while fetching the latest data from Parse.
Cloud Code allows you to run custom JavaScript logic on Parse Server instead of on the client.
You can use this to offload processing to the Parse servers thus increasing your app’s perceived performance. You can create hooks that run whenever an object is saved or deleted. This is useful if you want to validate or sanitize your data. You can also use Cloud Code to modify related objects or kick off other processes such as sending off a push notification.
We saw examples of limiting the data returned by writing restrictive queries. You can also use Cloud Functions to help limit the amount of data returned to your app. In the following example, we use a Cloud Function to get a movie’s average rating:
Parse.Cloud.define("averageStars", async (request) => {
const query = new Parse.Query("Review");
query.equalTo("movie", request.params.movie);
const results = await query.find();
let sum = 0;
for (let i = 0; i < results.length; ++i) {
sum += results[i].get("stars");
}
return sum / results.length;
});
You could have ran a query on the Review class on the client, returned only the stars field data and computed the result on the client. As the number of reviews for a movie increases you can see that the data being returned to the device using this methodology also increases. Implementing the functionality through a Cloud Function returns the one result if successful.
As you look at optimizing your queries, you’ll find that you may have to change the queries - sometimes even after you’ve shipped your app to the App Store or Google Play. The ability to change your queries without a client update is possible if you use Cloud Functions. Even if you have to redesign your schema, you could make all the changes in your Cloud Functions while keeping the client interface the same to avoid an app update. Take the average stars Cloud Function example from before, calling it from a client SDK would look like this:
[PFCloud callFunctionInBackground:@"averageStars"
withParameters:@{@"movie": @"The Matrix"}
block:^(NSNumber *ratings, NSError *error) {
if (!error) {
// ratings is 4.5
}
}];
PFCloud.callFunctionInBackground("averageStars", withParameters: ["movie": "The Matrix"]) {
(ratings, error) in
if !error {
// ratings is 4.5
}
}
If later on, you need to modify the underlying data model, your client call can remain the same, as long as you return back a number that represents the ratings result.
When counting objects frequently, instead consider storing a count variable in the database that is incremented each time an object is added. Then, the count can quickly be retrieved by simply retrieving the variable stored.
Suppose you are displaying movie information in your app and your data model consists of a Movie class and a Review class that contains a pointer to the corresponding movie. You might want to display the review count for each movie on the top-level navigation screen using a query like this:
PFQuery *query = [PFQuery queryWithClassName:@"Review"];
[query whereKey:@"movie" equalTo:movie];
[query countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
if (!error) {
// Request succeeded
}
}];
let query = PFQuery.queryWithClassName("Review")
query.whereKey("movie", equalTo: movie)
query.countObjectsInBackgroundWithBlock {
(number, error) in
if !error {
// Request succeeded
}
}
If you run the count query for each of the UI elements, they will not run efficiently on large data sets. One approach to avoid using the count()
operator could be to add a field to the Movie class that represents the review count for that movie. When saving an entry to the Review class you could increment the corresponding movie’s review count field. This can be done in an afterSave
handler:
Parse.Cloud.afterSave("Review", function(request) {
// Get the movie id for the Review
var movieId = request.object.get("movie").id;
// Query the Movie represented by this review
var Movie = Parse.Object.extend("Movie");
var query = new Parse.Query(Movie);
query.get(movieId).then(function(movie) {
// Increment the reviews field on the Movie object
movie.increment("reviews");
movie.save();
}, function(error) {
throw "Got an error " + error.code + " : " + error.message;
});
});
Your new optimized query would not need to look at the Review class to get the review count:
PFQuery *query = [PFQuery queryWithClassName:@"Movie"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// Results include the reviews count field
}
}];
let query = PFQuery.queryWithClassName("Movie")
query.findObjectsInBackgroundWithBlock {
(objects, error) in
if !error {
// Results include the reviews count field
}
}
You could also use a separate Parse Object to keep track of counts for each review. Whenever a review gets added or deleted, you can increment or decrement the counts in an afterSave
or afterDelete
Cloud Code handler. The approach you choose depends on your use case.
As mentioned previously, MongoDB is not efficient for doing partial string matching. However, this is an important use case when implementing search functionality that scales well in production.
Simplistic search algorithms simply scan through all the class data and executes the query on each entry. The key to making searches run efficiently is to minimize the number of data that has to be examined when executing each query by using an index as we’ve outlined earlier. You’ll need to build your data model in a way that it’s easy for us to build an index for the data you want to be searchable. For example, string matching queries that don’t match an exact prefix of the string won’t be able to use an index leading to timeout errors as the data set grows.
Let’s walk through an example of how you could build an efficient search. You can apply the concepts you learn in this example to your use case. Say your app has users making posts, and you want to be able to search those posts for hashtags or particular keywords. You’ll want to pre-process your posts and save the list of hashtags and words into array fields. You can do this processing either in your app before saving the posts, or you can use a Cloud Code beforeSave
hook to do this on the fly:
var _ = require("underscore");
Parse.Cloud.beforeSave("Post", request => {
var post = request.object;
var toLowerCase = function(w) { return w.toLowerCase(); };
var words = post.get("text").split(/\b/);
words = _.map(words, toLowerCase);
var stopWords = ["the", "in", "and"]
words = _.filter(words, function(w) {
return w.match(/^\w+$/) && ! _.contains(stopWords, w);
});
var hashtags = post.get("text").match(/#.+?\b/g);
hashtags = _.map(hashtags, toLowerCase);
post.set("words", words);
post.set("hashtags", hashtags);
});
This saves your words and hashtags in array fields, which MongoDB will store with a multi-key index. There are some important things to notice about this. First of all it’s converting all words to lower case so that we can look them up with lower case queries, and get case insensitive matching. Secondly, it’s filtering out common words like ‘the’, ‘in’, and ‘and’ which will occur in a lot of posts, to additionally reduce useless scanning of the index when executing the queries.
Once you’ve got the keywords set up, you can efficiently look them up using “All” constraint on your query:
PFQuery *query = [PFQuery queryWithClassName:@"Post"];
[query whereKey:@"hashtags" containsAllObjectsInArray:@[@"#parse", @"#ftw"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// Request succeeded
}
}];
let query = PFQuery.queryWithClassName("Post")
query.whereKey("hashtags", containsAllObjectsInArray: ["#parse", "#ftw"])
query.findObjectsInBackgroundWithBlock {
(objects, error) in
if !error {
// Request succeeded
}
}
There are some limits in place to ensure the API can provide the data you need in a performant manner. We may adjust these in the future. Please take a moment to read through the following list:
Objects
Queries
limit
parameter to change this.equalTo
constraints over the same key with two different values, which contradicts itself (perhaps you’re looking for ‘contains’).$exists: false
is not advised.each
query method in the JavaScript SDK cannot be used in conjunction with queries using geo-point constraints.containsAll
query constraint can only take up to 9 items in the comparison array.Push Notifications
Cloud Code
params
payload that is passed to a Cloud Function is limited to 50 MB.The following is a list of all the error codes that can be returned by the Parse API. You may also refer to RFC2616 for a list of http error codes. Make sure to check the error message for more details.
Name | Code | Description |
---|---|---|
UserInvalidLoginParams |
101 | Invalid login parameters. Check error message for more details. |
ObjectNotFound |
101 | The specified object or session doesn’t exist or could not be found. Can also indicate that you do not have the necessary permissions to read or write this object. Check error message for more details. |
InvalidQuery |
102 | There is a problem with the parameters used to construct this query. This could be an invalid field name or an invalid field type for a specific constraint. Check error message for more details. |
InvalidClassName |
103 | Missing or invalid classname. Classnames are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters. |
MissingObjectId |
104 | An unspecified object id. |
InvalidFieldName |
105 | An invalid field name. Keys are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters. Some field names may be reserved. Check error message for more details. |
InvalidPointer |
106 | A malformed pointer was used. You would typically only see this if you have modified a client SDK. |
InvalidJSON |
107 | Badly formed JSON was received upstream. This either indicates you have done something unusual with modifying how things encode to JSON, or the network is failing badly. Can also indicate an invalid utf-8 string or use of multiple form encoded values. Check error message for more details. |
CommandUnavailable |
108 | The feature you tried to access is only available internally for testing purposes. |
NotInitialized |
109 | You must call Parse.initialize before using the Parse library. Check the Quick Start guide for your platform. |
ObjectTooLarge |
116 | The object is too large. |
ExceededConfigParamsError |
116 | You have reached the limit of 100 config parameters. |
InvalidLimitError |
117 | An invalid value was set for the limit. Check error message for more details. |
InvalidSkipError |
118 | An invalid value was set for skip. Check error message for more details. |
OperationForbidden |
119 | The operation isn’t allowed for clients due to class-level permissions. Check error message for more details. |
CacheMiss |
120 | The result was not found in the cache. |
InvalidNestedKey |
121 | An invalid key was used in a nested JSONObject. Check error message for more details. |
InvalidACL |
123 | An invalid ACL was provided. |
InvalidEmailAddress |
125 | The email address was invalid. |
DuplicateValue |
137 | Unique field was given a value that is already taken. |
InvalidRoleName |
139 | Role’s name is invalid. |
ReservedValue |
139 | Field value is reserved. |
ExceededCollectionQuota |
140 | You have reached the quota on the number of classes in your app. Please delete some classes if you need to add a new class. |
ScriptFailed |
141 | Cloud Code script failed. Usually points to a JavaScript error. Check error message for more details. |
FunctionNotFound |
141 | Cloud function not found. Check that the specified Cloud function is present in your Cloud Code script and has been deployed. |
JobNotFound |
141 | Background job not found. Check that the specified job is present in your Cloud Code script and has been deployed. |
ValidationFailed |
142 | Cloud Code validation failed. |
WebhookError |
143 | Webhook error. |
InvalidImageData |
150 | Invalid image data. |
UnsavedFileError |
151 | An unsaved file. |
InvalidPushTimeError |
152 | An invalid push time was specified. |
HostingError |
158 | Hosting error. |
InvalidEventName |
160 | The provided analytics event name is invalid. |
ClassNotEmpty |
255 | Class is not empty and cannot be dropped. |
AppNameInvalid |
256 | App name is invalid. |
MissingAPIKeyError |
902 | The request is missing an API key. |
InvalidAPIKeyError |
903 | The request is using an invalid API key. |
Name | Code | Description |
---|---|---|
IncorrectType |
111 | A field was set to an inconsistent type. Check error message for more details. |
InvalidChannelName |
112 | Invalid channel name. A channel name is either an empty string (the broadcast channel) or contains only a-zA-Z0-9_ characters and starts with a letter. |
InvalidSubscriptionType |
113 | Bad subscription type. Check error message for more details. |
InvalidDeviceToken |
114 | The provided device token is invalid. |
PushMisconfigured |
115 | Push is misconfigured in your app. Check error message for more details. |
PushWhereAndChannels |
115 | Can’t set channels for a query-targeted push. You can fix this by moving the channels into your push query constraints. |
PushWhereAndType |
115 | Can’t set device type for a query-targeted push. You can fix this by incorporating the device type constraints into your push query. |
PushMissingData |
115 | Push is missing a ‘data’ field. |
PushMissingChannels |
115 | Non-query push is missing a ‘channels’ field. Fix by passing a ‘channels’ or ‘query’ field. |
ClientPushDisabled |
115 | Client-initiated push is not enabled. Check your Parse app’s push notification settings. |
RestPushDisabled |
115 | REST-initiated push is not enabled. Check your Parse app’s push notification settings. |
ClientPushWithURI |
115 | Client-initiated push cannot use the “uri” option. |
PushQueryOrPayloadTooLarge |
115 | Your push query or data payload is too large. Check error message for more details. |
InvalidExpirationError |
138 | Invalid expiration value. |
MissingPushIdError |
156 | A push id is missing. Deprecated. |
MissingDeviceTypeError |
157 | The device type field is missing. Deprecated. |
Name | Code | Description |
---|---|---|
InvalidFileName |
122 | An invalid filename was used for Parse File. A valid file name contains only a-zA-Z0-9_. characters and is between 1 and 128 characters. |
MissingContentType |
126 | Missing content type. |
MissingContentLength |
127 | Missing content length. |
InvalidContentLength |
128 | Invalid content length. |
FileTooLarge |
129 | File size exceeds maximum allowed. |
FileSaveError |
130 | Error saving a file. |
FileDeleteError |
153 | File could not be deleted. |
FileDeleteUnnamedError |
161 | Unnamed file could not be deleted. |
Name | Code | Description |
---|---|---|
InvalidInstallationIdError |
132 | Invalid installation id. |
InvalidDeviceTypeError |
133 | Invalid device type. |
InvalidChannelsArrayError |
134 | Invalid channels array value. |
MissingRequiredFieldError |
135 | Required field is missing. |
ChangedImmutableFieldError |
136 | An immutable field was changed. |
Name | Code | Description |
---|---|---|
ReceiptMissing |
143 | Product purchase receipt is missing. |
InvalidPurchaseReceipt |
144 | Product purchase receipt is invalid. |
PaymentDisabled |
145 | Payment is disabled on this device. |
InvalidProductIdentifier |
146 | The product identifier is invalid. |
ProductNotFoundInAppStore |
147 | The product is not found in the App Store. |
InvalidServerResponse |
148 | The Apple server response is not valid. |
ProductDownloadFilesystemError |
149 | The product fails to download due to file system error. |
Name | Code | Description |
---|---|---|
UsernameMissing |
200 | The username is missing or empty. |
PasswordMissing |
201 | The password is missing or empty. |
UsernameTaken |
202 | The username has already been taken. |
UserEmailTaken |
203 | Email has already been used. |
UserEmailMissing |
204 | The email is missing, and must be specified. |
UserWithEmailNotFound |
205 | A user with the specified email was not found. |
SessionMissing |
206 | A user object without a valid session could not be altered. |
MustCreateUserThroughSignup |
207 | A user can only be created through signup. |
AccountAlreadyLinked |
208 | An account being linked is already linked to another user. |
InvalidSessionToken |
209 | The device’s session token is no longer valid. The application should ask the user to log in again. |
Name | Code | Description |
---|---|---|
LinkedIdMissing |
250 | A user cannot be linked to an account because that account’s id could not be found. |
InvalidLinkedSession |
251 | A user with a linked (e.g. Facebook or Twitter) account has an invalid session. Check error message for more details. |
InvalidGeneralAuthData |
251 | Invalid auth data value used. |
BadAnonymousID |
251 | Anonymous id is not a valid lowercase UUID. |
FacebookBadToken |
251 | The supplied Facebook session token is expired or invalid. |
FacebookBadID |
251 | A user with a linked Facebook account has an invalid session. |
FacebookWrongAppID |
251 | Unacceptable Facebook application id. |
TwitterVerificationFailed |
251 | Twitter credential verification failed. |
TwitterWrongID |
251 | Submitted Twitter id does not match the id associated with the submitted access token. |
TwitterWrongScreenName |
251 | Submitted Twitter handle does not match the handle associated with the submitted access token. |
TwitterConnectFailure |
251 | Twitter credentials could not be verified due to problems accessing the Twitter API. |
UnsupportedService |
252 | A service being linked (e.g. Facebook or Twitter) is unsupported. Check error message for more details. |
UsernameSigninDisabled |
252 | Authentication by username and password is not supported for this application. Check your Parse app’s authentication settings. |
AnonymousSigninDisabled |
252 | Anonymous users are not supported for this application. Check your Parse app’s authentication settings. |
FacebookSigninDisabled |
252 | Authentication by Facebook is not supported for this application. Check your Parse app’s authentication settings. |
TwitterSigninDisabled |
252 | Authentication by Twitter is not supported for this application. Check your Parse app’s authentication settings. |
InvalidAuthDataError |
253 | An invalid authData value was passed. Check error message for more details. |
LinkingNotSupportedError |
999 | Linking to an external account not supported yet with signup_or_login. Use update instead. |
Name | Code | Description |
---|---|---|
ConnectionFailed |
100 | The connection to the Parse servers failed. |
AggregateError |
600 | There were multiple errors. Aggregate errors have an “errors” property, which is an array of error objects with more detail about each error that occurred. |
FileReadError |
601 | Unable to read input for a Parse File on the client. |
XDomainRequest |
602 | A real error code is unavailable because we had to use an XDomainRequest object to allow CORS requests in Internet Explorer, which strips the body from HTTP responses that have a non-2XX status code. |
Name | Code | Description |
---|---|---|
RequestTimeout |
124 | The request was slow and timed out. Typically this indicates that the request is too expensive to run. You may see this when a Cloud function did not finish before timing out, or when a Parse.Cloud.httpRequest connection times out. |
InefficientQueryError |
154 | An inefficient query was rejected by the server. Refer to the Performance Guide and slow query log. |
Name | Code | Description |
---|---|---|
OtherCause |
-1 | An unknown error or an error unrelated to Parse occurred. |
InternalServerError |
1 | Internal server error. No information available. |
ServiceUnavailable |
2 | The service is currently unavailable. |
ClientDisconnected |
4 | Connection failure. |
In order to provide better organization and avoid conflicts with Parse Platform’s built-in Parse.Error
codes, the following ranges are defined:
<= 4999
(including negative numbers)>= 5000 and <= 8999
>= 9000 and <= 9999
1
and add any specific information in the error message, or use another pre-defined error code of Parse Platform.