iOS Guide

Make amazing apps for iPhone, iPads and OSX.

Getting started

Using Appstax.framework in an existing project

The steps above assume that you are starting a new project from scratch. It is of course also possible to use Appstax in an existing iOS project or with a different project template in XCode. Just follow these steps:

  • Open appstax-ios folder you downloaded from GitHub.
  • Drag Appstax.framework into your project in XCode. Select "Copy items if needed".
  • Drag Vendor/Starscream.framework into your project the same way.
  • Open the "General" settings for your app target and add both frameworks to the "Embedded Binaries" list.
  • Switch to "Build settings" and turn on "Embedded Contents Contains Swift Code"

Initialize Appstax

Before making any API calls, you need to initialize the framework. Add this code to application:didFinishLaunchingWithOptions: in your AppDelegate, replacing YourAppKey with the AppKey you received when signing up.

[Appstax setAppKey:@"YourAppKey"];

Working with objects

Create & save objects

Say we want to create an object for storing contact information, then we just add the following lines of code:

AXObject *contact = [AXObject create:@"Contacts"];
contact[@"name"]  = @"John Appleseed";
contact[@"email"] = @"john@appleseed.com";
[contact save];

In line 1, we create an object that will be stored to the Contacts collection. Line 2 and 3 set the name and email properties of our new object. And finally, in line 4 we save the object to the Appstax server.

Before you can create objects from code you must define your collection using the admin web interface.

So, for example, if you want to start saving product objects to Appstax you must first log on to the admin and create a collection named products.

Remember that collection names are case sensitive, so a collection named Products won't match [AXObject create:@"products"]

Asynch communication and completion handlers

The [contact save] call in the previous example kicks off an asynchronous HTTP call to the server and immediately returns control to your next line of code in order to avoid blocking the main thread and freezing up your UI.

If you want to know when the save has completed, you can use this method and provide a completion block instead:

[contact save:^(NSError *error) {
    if(!error) {
        // ... object has been saved
    }
}];

Load all objects in a collection

If you want to load all the objects of a collection you just:

[AXObject findAll:@"Contact"
       completion:^(NSArray *objects, NSError *error) {
           if(!error) {
               // ... objects contains all your AXObject* instances
           }
}];

Using the returned objects

The returned array of objects is not retained by Appstax, and you can use it in any way you like. For example, you could store it in a property you define on a UITableViewController and look up individual objects to render in cells:

// Fetch the data when the view has loaded
- (void)viewDidLoad {
    [super viewDidLoad];
    [AXObject findAll:@"Contact"
           completion:^(NSArray *objects, NSError *error) {
               if(!error) {
                   _contacts = objects; // keep reference to objects in a property
                   [self.tableView reloadData];
               }
    }];
}

// Use properties from your objects in a table cell.
// In this example the cells are set to UITableViewCellStyleSubtitle in interface builder

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"
                                                            forIndexPath:indexPath];

    AXObject *contact = _contacts[indexPath.row];  // Find the object to display
    cell.textLabel.text = contact[@"name"];        // Assign the property values
    cell.detailTextLabel.text = contact[@"email"]; // to the cell labels
    return cell;
}

Object id

The id of an object is accessible in a read-only property object.objectID:

AXObject *object = [AXObject create:@"Contacts"];

// objectID is nil before it has been saved
NSLog(@"Object id: %@", object.objectID);

[object save:^(NSError *error) {
    // objectID matches the sysObjectID column in the databrowser.
    NSLog(@"Object id: %@", object.objectID);
}];

Load a single object

For loading a single object the following lines will do:

[AXObject find:@"Contact"
        withId:@"1234-3456-234-54321"
    completion:^(AXObject *object, NSError *error) {
        if(!error) {
            // ... object contains your AXObject* instance
        }
}];

Remove an object

Just call remove: and it will be deleted from your datastore:

[contact remove:^(NSError *error) {
    if(!error) {
        // ... object has been removed
    }
}];

Refresh an object

If you already have an object reference but the data may have changed on the server, you can refresh it:

[contact refresh:^(NSError *error) {
    if(!error) {
        // ... the contact object has been updated
    }
}];

Querying Objects

There are several ways to find a selection of objects in a collection. We provide a generic query block that can be used for any request, and more specific methods for the most common use cases.

Matching property values

You can specify a dictionary of properties and values to match:

[AXObject find:@"Friends"
          with:@{@"Gender":@"Male",
                 @"Hometown":@"Oslo"}
    completion:^(NSArray *objects, NSError *error) {
          if(!error) {
              // ... objects contains all male friends from Oslo
          }
}];

... or you can use the generic query block:

[AXObject find:@"Friends" query:^(AXQuery *query) {
      [query string:@"Gender" equals:@"Male"];
      [query string:@"Hometown" equals:@"Oslo"];
  } completion:^(NSArray *objects, NSError *error) {
      if(!error) {
         // ... objects contains all male friends from Oslo
      }
}];

Searching string values

If you want your users to search your data you can use search:, and provide the string values to search for:

[AXObject find:@"Friends"
        search:@{@"Description":@"music"}
    completion:^(NSArray *objects, NSError *error) {
          if(!error) {
              // ... Friends with 'music' in their description
          }
}];

... or you can use the generic query block:

[AXObject find:@"Friends" query:^(AXQuery *query) {
    [query string:@"Description" contains:@"music"];
  } completion:^(NSArray *objects, NSError *error) {
      if(!error) {
          // ... Friends with 'music' in their description
      }
}];

You can also easily search for the same value in many properties at once:

[AXObject find:@"Articles"
        search:@"music"
    properties:@[@"Title",@"Content",@"Tags"]
    completion:^(NSArray *objects, NSError *error) {
        if(!error) {
            // .. Articles with 'music' in Title, Content or Tags
        }
}];

Handling users

Security note: Make sure you review the users collection permissions in the Admin UI. If you are using email addresses as usernames or are otherwise storing sensitive information in user objects, set the "read" permissions to "owner" or "admin" level.

Using the standard signup/login screens

This is all you need to do to add signup/login to your app:

// Add this code to -viewDidLoad in your first ViewController

[AXUser requireLogin:^(AXUser *user) {
     // user is logged in.
}];

This will show the standard signup/login screen and call the completion handler after the user has successfully signed up or logged in. If the user has previously used the app, the existing session will be restored and the completion handler is called without showing the login screen.

Customize the signup/login UI

You can change the appearance by setting properties on the existing backgrounds:

// This example just sets the background color

[AXUser requireLogin:^(AXUser *user) {

} withCustomViews:^(AXLoginViews *views) {
    views.signup.backgroundColor = [UIColor lightGrayColor];
    views.login.backgroundColor  = [UIColor lightGrayColor];
}];

Or you can replace the standard backgrounds with your own. Look at the AXNotes example in the SDK to see this in action:

// This example loads new background views from a nib

[AXUser requireLogin:^(AXUser *user) {

} withCustomViews:^(AXLoginViews *views) {
    NSArray *nibViews = [[NSBundle mainBundle] loadNibNamed:@"LoginViews"
                                                      owner:self options:nil];
    views.signup = nibViews[0];
    views.login = nibViews[1];
}];

Create your own signup/login process

If you want to make your own user interface for signup and login completely from scratch, you need to use the following methods:

[AXUser signupWithUsername:@"user@example.com"
                  password:@"myspecialsecret"
                completion:^(AXUser *user, NSError *error) {
    if(!error) {
        // ... *user is registered and logged in!
    }
}];

[AXUser loginWithUsername:@"user@example.com"
                 password:@"myspecialsecret"
               completion:^(AXUser *user, NSError *error) {
    if(!error) {
        // ... *user is logged in
    }
}];

Social Login

If you want to let your users log in with a social network like Facebook, you can configure a list of providers to use in your call to requireLogin:

[AXUser requireLogin:^(AXLoginConfig *config) {
  config.providers = @[@"facebook", @"google"];
} completion:^(AXUser *user) {
  // user is logged in
}];

Or, if you are creating your own login UI, you can do the following:

[AXUser loginWithProvider:@"facebook" completion:^(AXUser *user, NSError *error) {
  if(error == nil) {
    // The user is logged in with a Facebook account
  } else {
    // Something went wrong. The user may have declined access for your app.
  }
}];

Please read our Social Login Guide for a complete description of how to use Social Login.

Logout

The session will be invalidated on the server and later calls to [AXUser currentUser] will return nil:

[AXUser logout];

Password reset

You can set up a password reset flow by using the requestPasswordReset and changePasssword functions.

Create a text field where user can provide an email address, and start the process:

NSString *email = @""; // Replace with email from text field

[AXUser requestPasswordReset:email completion:^(NSError *error) {
  if(error == nil) {
    // password reset email has been sent to user
  } else {
    // ... something went wrong
  }
}];

An email will be sent to the user containing a code that can then be used to specify a new password. You need to log in to the AppStax Admin UI to set up the contents of this password reset email.

Now set up a second set of text fields where the user can use the received code to set a new password, and send the values like this:

[AXUser changePassword:@""  // Get these values from your text fields
              username:@""  // Get these values from your text fields
                  code:@""  // Get these values from your text fields
                 login:NO   // Set this to YES to automatically log in
            completion:^(AXUser *user, NSError *error) {
                if(error == nil) {
                  // Password was successfully changed.
                  // *user will be set if you specified login: YES
                } else {
                  // ... something went wrong
                }
}];

Note that the pin code will only be valid for 10 minutes. Note also that password reset currently only works if users provide emails as their usernames.

Add more data to user objects

You can add extra properties to user objects. This data will be saved in a special users collection, so make sure you add the appropriate columns in the Data browser first:

// This example assumes the user is already logged in
AXUser *user = [AXUser currentUser];
user[@"gender"] = @"Male";
user[@"hometown"] = @"New York";
[user save];

The save method can also take a completion handler:

[user save:(NSError *error) {
    if(!error) {
        // ... *user was saved successfully
    }
}];

Controlling access to objects

When you create new collections, all users can create and read objects from it. While this enables you to get started quickly, it may not be what you want in an app with authenticated users. To restrict access to objects and only allow explicit permissions per object, log in to the Admin UI and set the create and write collection permissions to "only owners of the objects". You can then use the API described below to grant object permissions.

Sharing objects between users

For two users to share an object, the user that created it must grant permissions on the object with the username of the other user:

// You must provide the means for the user to input a username
// to share with (like a text field or a friend list.)
// In our example the object will be shared with a user "buddy"
NSString *username = @"buddy";

AXObject *note = [AXObject create:@"Notes"];
[note grant:username permissions:@[@"read",@"update"]];
[note save];

The example creates a note and gives "read" and "update" permissions, but "buddy" will not be able to delete the note or give anyone else access. The following table lists all permissions a user could have for an object:

With permission ... ... user is able to
"read" Load the object and view all its properties. It will be included in queries.
"update" Change property values on the object and save it.
"delete" Delete the object from storage.
"grant read" Grant and revoke "read" access to others.
"grant update" Grant and revoke "update" access to others.
"grant delete" Grant and revoke "delete" access to others.

The previous example granted permissions on a new object, but of course you can do it on existing objects as well. Just remember to call [object save] to persist the permission:

[AXObject find:@"Posts"
        withId:@"123-4567"
        completion:^(AXObject *object, NSError *error) {
            [object grant:@"buddy" permissions:@[@"read"]];
            [object save];
        }]

Revoking permissions

Removing someones access to an object is just as easy. If "buddy" has "read" and "update" permissions, the following would leave him with just "read" permissions:

[note revoke:@"buddy" permissions:@[@"update"]];
[note save];

Public access

In some applications you will want everyone, including anonymous users, to see or interact with objects. Some use cases are wikis, blogging and public photo sharing applications. Objects have special methods for granting and revoking public access:

// Publishing an blog post
[post grantPublic:@[@"read"]];
[post save];

// Removing post from public
[post revokePublic:@[@"read"]];
[post save];

// Creating a wiki article anyone can edit
[article grantPublic:@[@"read",@"update"]];
[article save];

Working with files

You add files to your application by using the file column type in the Data browser and the AXFile in the SDK. You can create files from a path on disk, NSData or UIImage objects:

// If you have a path to a resource on the file system
NSString *path = [[NSBundle mainBundle] pathForResource:@"MyData" ofType:"txt"];
AXFile *file = [AXFile fileWithPath:path];

// If you have NSData
NSData *data = [@"Hello files!" dataUsingEncoding:NSUTF8StringEncoding];
AXFile *file = [AXFile fileWithData:data name:@"hello.txt"];

// If you have a UIImage
UIImage *image = ...
AXFile *file = [AXFile fileWithImage:image name:@"MyImage.png"];

Saving files

Now let's say you add an Attachment column to your Notes collection. You then let your user upload a file by assigning it to the attachment property and saving the note object.

// Create a note
AXObject *note = [AXObject create:@"Notes"];
note[@"Title"] = @"My note with attachment";

// Create an AXFile instance and set as 'Attachment' property
NSString *path = ...
note[@"Attachment"] = [AXFile fileWithPath:path];

// Saving the object also uploads the file(s) belonging to the object
[note save:^(NSError *error) {
     if(!error) {
         // the note is saved and the attachment uploaded
     }
}];

Loading the file data

File data is not included when loading objects. Otherwise, loading objects would cause a lot of unnecessary network traffic. To load the contents of a file, you call load: on it:

AXObject *note = ... // an object loaded from the server
AXFile *attachment = note[@"Attachment"];

[attachment load:^(AXError *error) {
    if(!error) {
        // attachment.data now contains the file data
    }
}];

Displaying images

If the file is an image and you just want it displayed on screen, you don't have to manage the data loading yourself. The AXImageView will load image data in the background and display it when it's ready:

AXObject *profile = ... // an object loaded from the server
AXFile *file = profile[@"BackgroundImage"];
AXImageView *imageView = [AXImageView viewWithFile:file];

If you already have a UIImageView added to interface builder, just change its class to the AXImageView subclass and specify the image file in your view controller like this:

AXFile *file = profile[@"BackgroundImage"];
[self.myImageView loadFile:file];

Getting smaller image sizes

When displaying image files in your app it is often desirable to save bandwidth and load a lower resolution image than the user originally uploaded. The AXImageView will handle this for you:

// load and display a resized and cropped 150x100 version of the image
AXImageView *thumbnail = [AXImageView viewWithFile:file size:CGSize(150, 100) crop:YES];

// you can do the same with interface builder
[self.myImageView loadFile:file size:CGSizeMake(150, 100) crop:YES];

Relations

As your data model grows, you will often need objects to reference other objects. There are two types of relations you can add:

  • A single relation adds a property that can reference one other appstax object.
  • An array relation adds an array where you can add as many appstax objects as you like.

If you come from a database modelling background you will find single relations useful for creating one-to-one and many-to-one models, while array relations work well for one-to-many and many-to-many models.

Single relations (one-to-one / many-to-one)

Given two collections invoices and customers, we can add a .customer single relation to the invoices collection in order for an invoice to include customer information without duplicating the details into all the invoices.

To create the relation in the databrowser: Select the invoices collection, click the relations button.

To create the relation on the command line, run appstax relation invoices.customer customers

Let's create a new customer and his first invoice and save them both with saveAll:

// create customer and invoice objects
AXObject *customer = [AXObject create:@"customer"];
AXObject *invoice  = [AXObject create:@"invoices"];
customer[@"name"] = @"Bill Buyer";
customer[@"address"] = @"Money Hill";
invoice[@"total"] = @199;

// assign the related customer object and save both objects
invoice[@"customer"] = customer;
[invoice saveAll:^(NSError *error) {
    if(error == nil) {
        // ... both objects have been saved
    }
}];

Choosing between [object save] and [object saveAll]

An object needs to be saved before it can be used in a relation, and calling save will fail if it finds relations with unsaved objects. You can choose to call save on each object yourself, or call saveAll to have all the related objects saved automatically.

To load invoices and include related customer data you can use the expand option with find/findAll:

[AXObject findAll:@"invoices" 
          options:@{@"expand":@YES}
       completion:^(NSArray *invoices, NSError *error) {
           if(error == nil) {
               // now you can use invoices[0][@"customer"][@"name"] etc.
           }
       }];

You can find all invoices for a specific customer with a property matching query:

AXObject *myCustomer = ... // myCustomer has previously been loaded from the backend
[AXObject find:@"invoices" 
          with:@{@"customer": myCustomer}
          completion:^(NSArray *invoices, NSError *error) {
              if(error == nil) {
                  // ... render the invoices in your view
              }
          }];

// add the expand option to include the related customer objects:
[AXObject find:@"invoices" 
          with:@{@"customer": myCustomer}
       options:@{@"expand":@YES}
    completion:^(NSArray *invoices, NSError *error) {
        if(error == nil) {
            // ... render the invoices in your view,
            // including invoices[0][@"customer"][@"name"] etc.
        }
    }];

Loading nested relations

When adding the @"expand":@YES option the objects returned will include the first level of related objects. Those related objects may themselves have relations to other objects. You can include nested relations by specifying the number of levels you want:

@{@"expand":@YES} // one level
@{@"expand":@1}   // one level
@{@"expand":@2}   // two levels
@{@"expand":@10}  // ten levels
... etc ...

If the customer collection in the previous example included a relation to a contacts collection, you could include all contacts for an invoice like this:

[AXObject find:@"invoices" 
          with:@{@"customer": myCustomer}
       options:@{@"expand":@2}
    completion:^(NSArray *invoices, NSError *error) {
        if(error == nil) {
            // ... render the invoices in your view,
            // including invoices[0][@"customer"][@"contacts"][0][@"email"] etc.
        }
    }];

Array relations (one-to-many / many-to-many)

Given two collections blogs and posts, we can connect the posts to a blog by adding a .posts[] array relation to the blog collection.

To create the relation in the databrowser: Select the blogs collection and click the relations button.

To create the relation on the command line, run appstax relation blogs.posts[] posts

Let's create a new blog and a couple of posts:

// create the blog
AXObject *blog = [AXObject create:@"blogs"];
blog[@"title"] = @"Zen";

// create a post
AXObject *post1 = [AXObject create:@"posts"];
post1[@"title"] = @"My first post";
post1[@"content"] = @"...";

// create another post
AXObject *post2 = [AXObject create:@"posts"];
post2[@"title"] = @"My second post";
post2[@"content"] = @"...";

// add posts to blog and save
blog[@"posts"] = @[post1, post2];
[blog saveAll]; // saves both posts and blog

We can now load all blogs, including posts, with a query using find/findAll and expand:

[AXObject findAll:@"blogs"
          options:@{@"expand":@YES}
       completion:^(NSArray *blogs, NSError *error) {
           if(error == nil) {
               // now you can use
               // blogs[0][@"title"]
               // blogs[0][@"posts"][0][@"title"]
               // ... etc ...
           }
       }];

Relations and access control

Querying relations

Querying objects with relations enforces "read" permissions. For example, a blog.posts[] relation may have many posts, but a call to find:@"blogs" withId:@"<blogid>" options:@{@"expand":@YES} will only include those posts the current user has "read" access to.

The kind of access restrictions you want on objects will affect the relations model you should use. Adding or removing related objects requires "create" or "update" permissions on the object where the relation is stored. In practise, this often means choosing between implementing one-to-many or many-to-one using array or single relations.

Example 1: If you have a blog data model with posts and comments collections, you may let users comment on posts by adding a post.comments[] array relation. But in order for users to add comments in this model they would need "update" permissions on the posts object. This is probably not what you want, as it would allow all users to change the contents of the blog post. Instead you should use a comment.post single relation, which will allow anyone to add a comment referencing any post.

Example 2: On the flip side of example 1 you may have blogs and posts in your data model and only want the owner of a blog to add posts to it. If you do this with a post.blog single relation, anyone would be able to add posts to the blog. If you instead choose a blog.posts[] array relation, adding posts will be restricted to users with "update" permissions to the blog.

Realtime Channels

Realtime channels is currently in beta. Drop us an email at support@appstax.com for comments and suggestions!

Channels use WebSockets to provide bidirectional communication between the backend and your app.

There are three types of channels: public channels, private channels, and object channels:

  • Public channels are open to all users. Anyone can send and receive messages.
  • Private channels lets you grant or revoke read/write access to specific users.
  • Object channels lets you use sockets to observe changes to a specific collection.

A channel identifier consists of its type, a slash, and then an name given by the developer:

// A public channel called 'foobar':
[AXChannel channel:@"public/foobar"];

// A private channel identified by an email:
[AXChannel channel:@"private/james@example.com"];

// A channel for updates to the "notes" collection:
[AXChannel channel:@"objects/notes"];

Public channels

Public channels are the simplest type of channel. Any user can subscribe to a public channel, read all messages sent to that channel, and send new messages to the other subscribers. Here's an example:

To send a message, call the send method. You can send a plain NSString or a NSDictionary object:

AXChannel *ch = [AXChannel channel:@"public/messages"];
[ch send:@{@"text":@"Hello World!"}];

To receive messages, register an event handler with the on method:

AXChannel *ch = [AXChannel channel:@"public/messages"];

[ch on:@"message" handler:^(AXChannelEvent *event) {
  // Called when a message arrives. The content is in event.message
}];

[ch on:@"error" handler:^(AXChannelEvent *event) {
  // Something went wrong
}];

For complete example of public channels in use, check out our chat tutorial.

Private channels

Private channels work just like public channels, but with access control. You can use grant and revoke to control read and write access.

// Private channels have their own prefix:
AXChannel *ch = [AXChannel channel:@"private/john"];

// Use grant/revoke to control access:
[ch grant:@"jane" permissions:@[@"read", @"write"]];

// You can now send messages:
[ch send:@"Hello Jane!"];

Security

The security in private channels come from two features:

  • The owner of a channel (the first user to use a private/... channel identifier) has complete control over who can send and receive messages on that channel.
  • The receiver of a message can verify who sent a message by checking the event.sender property. This is set by the appstax realtime server and will always contain the username of the user that sendt the message.

The channel identifier can NOT be used to verify who is on the other end of a channel. This is because all authenticated users are free to create any channel identifier they wish. Use the security features described above instead.

Listening to multiple channels with wildcard patterns

You can listen to many channels at once by ending the channel identifier with a *

AXChannel *ch = [AXChannel channel:@"public/news/*"];

[ch on:@"message" handler:^(AXChannelEvent *event) {
  // Messages will be received from 
  // public/news/economy and public/news/technology, but not from
  // public/notifications or public/messages
}];

This also simplifies listening to private channels because the receiver does not have to know the name of the channel to listen to:


// Jane has created this channel:
AXChannel *ch = [AXChannel channel:@"private/chat/jane"];
[ch grant:@"john", @[@"read"]];

// Jim has created this channel:
AXChannel *ch = [AXChannel channel:@"private/chat/jim"];
[ch grant:@"john", @[@"read"]];

// and John can listen to both:
AXChannel *ch = [AXChannel channel:@"private/chat/*"];
[ch on:@"message" handler:^(AXChannelEvent *event) {
  // Here John will receive everything he's allowed to receive
  // on any channel starting with "private/chat/..."

  // event.sender will tell which user sent the message (username)
  // event.channel contains the full channel identifier used to send the message
}];

Object channels

Object channels lets your observe changes to a collection without resorting to periodic polling.

In the following example, the app listens for any additions, updates or deletions to objects in the notes collection:

// channel identifier is objects/<collection>
AXChannel *ch = [AXChannel channel:@"objects/notes"];

[ch on:@"object.created" handler:^(AXChannelEvent *event) {
  // a new object was added to the collection
  // access it with event.object
}];

[ch on:@"object.updated" handler:^(AXChannelEvent *event) {
  // event.object was updated
}];

[ch on:@"object.deleted" handler:^(AXChannelEvent *event) {
  // event.object was deleted
}];

These channels lets you display news or react to changes to objects in your collections, without having to check the server periodically for new updates. Unlike public and private channels, you can't send directly to an object channel. Use the normal appstax object methods like [object save] to perform changes to the collection.

If you're only interested in certain objects, you can add a filter to the channel:

AXChannel *ch = [AXChannel channel:@"objects/notes" filter:@"title like 'Hello%'"];

[ch on:@"object.created" handler:^(AXChannelEvent *event) {
  // only notes with title starting with Hello will arrive here
}];

The filter syntax is the same as the one used when querying objects.