My Query-Based Sync ToDo App

Overview

We will make a simple work management application called "ToDo", which will allow users to create "projects". Within each project users will be able to create any number of associated "tasks".

Realm works by defining a data model that configures the schema of a "realm". A realm is an instance of a Realm Mobile Database container. In this project, our client realm will connect and sync data with the centralized Realm Object Server through a process called query-based synchronization.

We will cover the concepts of:

  • Authenticating users with realm

  • Opening a realm database

  • Syncing data and setting notifications

  • Adding realm permissions

Want to get started right away with the complete source code? Check out our Github with ready-to-compile source code then follow the instructions in README.md to get started. Don't forget to update the Constants.swift file with your Realm Cloud or self-hosted instance URL before running the app.

Prerequisites:

This tutorial makes the assumption that you have an introductory understanding of either Swift or fundamental object-oriented programming principles. As such we will focus more on the implementation of the Realm specific API.

Before we get started we need a few things setup; the prerequisites for this project are:

  • Xcode 9.0 or later

  • CocoaPods v1.2.x+

  • A Realm address:

    • Cloud: the Realm Cloud instance URL that was generated when you created your instance (it can be found by logging in to https://cloud.realm.io, and clicking the Copy Instance URL link).

    • Self-hosted: the Realm address that your self-hosted Realm Object Server is running on.

  • Realm Studio (optional) -- throughout the tutorial we will test components of the sync process where directly viewing and editing the Realm Object Server is helpful.

This tutorial uses CocoaPods to install the required Realm frameworks. You can install CocoaPods by following the installation instructions at cocoapods.org.

Step 1: Step 1: Create a New iOS Project

Open Xcode, create a new iOS Project (we recommend the "Single View" application). Let's name it "iOSToDoApp." When prompted, save it to a convenient place such as your desktop.

Step 2: Initial Run Test

Before we dive into adding code, it's a good idea to make sure that Xcode is configured correctly and that we can successfully compile the app template we just created. In order to do this, select a simulator (for example the iPhoneX) from the build menu, then press the build/run icon. The app should build and launch and with an empty template no errors in the console

Step 3: Install the Realm Frameworks

To use Realm Cloud in your iOS app, you'll need to add the Realm and RealmSwift frameworks to your project. This is most easily accomplished using using either of the major iOS dependency managers: Cocoapods or Carthage. Alternatively, you can install the framework by downloading it directly from Realm and dragging it into your Xcode project. The tabs below give detailed instructions on how to install the Realm framework for each of these options.

.Cocoapods
Carthage
Untitled

Make sure you have Cocoapods version 1.2.x or higher.

If you run into any errors or if you need to install Cocoapods, please follow the installation instructions at Cocoapods.org.

If you choose to use Cocopods, you will need to close your Xcode project for the next steps; we will re-open it once Cocopods is set up.

Cocoapods uses a Podfile to track, load, and manage your project's external dependencies; to create the Podfile, open a new terminal window, change the directory to your newly created Xcode project. Type in:

pod init

This will create a new Podfile - open this file with an editor of your choice and look for the section that starts with # Pods for iOSToDoApp, then add the following line:

pod 'RealmSwift'

Save your changes, and then run the following command from the terminal window:

pod install --repo-update

This will download all the required packages and create a new Xcode workspace for your project. Once the Cocoapods installation process completes open the new iOSToDoApp.xcworkspace to continue to work on your project.

Make sure you have Carthage version 0.17.x or higher.

If you prefer Carthage, you can add the following to your Cartfile. You will need to create the Cartfile in your Xcode project directory, then add the following line:

github "realm/realm-cocoa"

Save your changes and then download and build the packages with this command in the terminal window:

carthage bootstrap

This process can take 10 minutes to complete and depending on your installed version of Xcode, you may see warning about compiler incompatibilities; these can be ignored.

After Carthage builds the Realm frameworks, drag RealmSwift.framework and Realm.framework files from the Carthage/Build/iOS directory to the “Linked Frameworks and Libraries” section of your Xcode project’s “General” settings tab. Click OK when prompted to copy the framework files.

In your application target’s “Build Phases” settings tab, click the + icon and choose “New Run Script Phase”. Create a Run Script with the following contents:

/usr/local/bin/carthage copy-frameworks

and add the paths to the frameworks under “Input Files”

$(SRCROOT)/Carthage/Build/iOS/Realm.framework$(SRCROOT)/Carthage/Build/iOS/RealmSwift.framework

In order to load the framework directly, you will need to download the latest version of the Realm framework from this link. Once the download has completed there will be a new folder in your Downloads directory named Realm-swift-x.y.z (where x.y.z is a version number). Open this folder, navigate to the latest swift version (at the time of this writing 4.02) and drag the Realm.Framework and RealmSwift.Framework file into the Frameworks and Libraries tab as shown here:

Test the Framework Build

Reminder: If you are using Cocoapods, you will need to close the Xcode iOSToDoApp.xcproj (which is the default project made when you created your app) and reopen the newly-created iOSToDoApp.xcworkspace to be able to successfully compile with the Realm framework.

At this point you will want to re-run the template project to ensure that the addition of the Realm Frameworks was successful. You won't see any changes in the app display in the simulator and the app should compile without error.

Step 4: Remove Unnecessary Files

The default app template contains a few files we won't need in this example, so we'll remove them to reduce any possible confusion.

  1. Open the Info.plist for your target (this will be in the iOSToDoApp group in the file navigator)

  2. Find the Main storyboard file base name entry and remove it by pressing on the - icon next to it in the property editor

  3. Find the Main.storyboard file in Xcode's file navigator and delete it by selecting it and then pressing the Delete key

Edit the AppDelegate.swift file and replace the func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) method to match the following code below. This sets up UINavigationController with a WelcomeViewController which we will create in an upcoming step.

While using the code snippets in this tutorial it is important to use the "copy to clipboard" button in the top right of each block. This helps prevent formatting errors in Xcode.

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = UINavigationController(rootViewController: WelcomeViewController())
return true
}

Step 5: Create a Constants file and Set the Realm Cloud Instance URL

Next we will create a new Swift file to contain our app's constants. This is done by selecting File > New > File... in Xcode and then selecting "Swift File" from the file type selector panel. Press next then enter Constants for the file name (the .swift extension will be added automatically) and navigate, if needed, to your Xcode project directory and save the file.

Paste the code snippet here into the newly created Constants.swift; replace the string MY_INSTANCE_ADDRESS with the hostname portion of the Realm Cloud instance you copied from the Realm Cloud Portal (e.g., mycoolapp.us1.cloud.realm.io). We will use these constants (e.g. Constants.AUTH_URL and Constants.REALM_URL) wherever we need to reference our Realm instance.

NOTE: The Realm Cloud Portal presents fully specified URLs (e.g., https://appname.cloud.realm.io); be sure to paste in only the host name part (e.g., appname.cloud.realm.io) into your copy of the Constants.swift file.

Self-Hosted: The code snippet below is optimized for cloud. When using a self-hosted version of Realm Object Server, directly set the AUTH_URL and REALM_URL variables. It is likely you won't initially have SSL/TLS setup, so be careful with http[s] and realm[s].

Constants.Swift
import Foundation
struct Constants {
// **** Realm Cloud Users:
// **** Replace MY_INSTANCE_ADDRESS with the hostname of your cloud instance
// **** e.g., "mycoolapp.us1.cloud.realm.io"
// ****
// ****
// **** ROS On-Premises Users
// **** Replace the AUTH_URL and REALM_URL strings with the fully qualified versions of
// **** address of your ROS server, e.g.: "http://127.0.0.1:9080" and "realm://127.0.0.1:9080"
static let MY_INSTANCE_ADDRESS = "MY_INSTANCE_ADDRESS" // <- update this
static let AUTH_URL = URL(string: "https://\(MY_INSTANCE_ADDRESS)")!
static let REALM_URL = URL(string: "realms://\(MY_INSTANCE_ADDRESS)/ToDo")!
}

Step 6: Add WelcomeViewController and Authentication Dialog

Create a new view controller called WelcomeViewController.swift. This will be the controller you see when the app starts up and will be used to log in to the app and connect your application to the Realm Cloud.

To create the view controller, select File > New > File... in Xcode. Select "Cocoa Touch Class" from the file type selector panel. Press next , then enter WelcomeViewController for the class name and UIViewController for the subclass name. Press next again, navigate to your Xcode project directory if necessary, and save the file.

Near the top of the new view controller file, below the existing import statement, import the Realm framework by adding this line:

import RealmSwift

Xcode may show errors next to the new import line or other lines; this is normal and these will disappear as we add code and the app is compiled.

Next, we will add a viewDidAppear method to the view controller with the snippet . below. The main function of this new method is to log a user in to your Realm Cloud instance. We are just going to login using a "nickname" you provide from a dialog that will be presented when the app launches.

How the WelcomeViewController Authenticates with Realm Cloud

Realm supports a number of authentication methods. The Nickname method, which we're using in this short introduction, does not require a login password, making it convenient for prototyping and extremely low-security applications. Nickname credentials are constructed with: let creds = SyncCredentials.nickname("sally", isAdmin: true)

You’ll notice that isAdmin is set to true; this allows this user full control over the Realm. We will revisit this later, but for the purposes of getting started quickly we recommend that you keep this set to true to encounter less friction when learning more about Realm.

Note: In a production setting you would want to turn this provider off so that random strangers could not log into the Realm Object Server admin interfaces.

The viewDidAppear method does not appear in the initial version of our WelcomeViewController: Add this new method after the closing brace of the stub viewDidLoad() method near the top of the file:

WelcomeViewController.swift
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
title = "Welcome"
if let _ = SyncUser.current {
// We have already logged in here!
self.navigationController?.pushViewController(ProjectsViewController(), animated: true)
} else {
let alertController = UIAlertController(title: "Login to Realm Cloud", message: "Supply a nice nickname!", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Login", style: .default, handler: { [unowned self]
alert -> Void in
let textField = alertController.textFields![0] as UITextField
let creds = SyncCredentials.nickname(textField.text!)
SyncUser.logIn(with: creds, server: Constants.AUTH_URL, onCompletion: { [weak self](user, err) in
if let _ = user {
self?.navigationController?.pushViewController(ProjectsViewController(), animated: true)
} else if let error = err {
fatalError(error.localizedDescription)
}
})
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alertController.addTextField(configurationHandler: {(textField : UITextField!) -> Void in
textField.placeholder = "A Name for your user"
})
self.present(alertController, animated: true, completion: nil)
}
}

The bulk of this code sets up your user's credential and connects to the Realm Cloud. If the login is successful, it then transitions to the ProjectsViewController, which we will build in the next several steps. In the event that a login attempt fails, this method will post a Realm error message dialog detailing what went wrong.

Step 7: Add the ToDo List Item and Project Realm Model

Adding the Item model

Before we get to the creation of the ToDo list we need to define the Realm model that will describe our ToDo list items. Create a new file called Item.swift and add in the model definition below.

As you did with the WelcomeViewController , create a new view controller by selecting File > New > File... This time, select "Swift File" from the file type selector panel. Press next, then enter Item for the file name (the .swift extension will be added automatically) and navigate to your Xcode project directory (if necessary) and save the file.

Item.swift
import RealmSwift
class Item: Object {
@objc dynamic var itemId: String = UUID().uuidString
@objc dynamic var body: String = ""
@objc dynamic var isDone: Bool = false
@objc dynamic var timestamp: Date = Date()
override static func primaryKey() -> String? {
return "itemId"
}
}

This Realm Model definition will hold each task of the ToDo list. All properties are required and have default values. We will use the timestamp property to sort the collection of Items.

Adding the Project model

The other class we will need to add to our schema is the Project. Continue by creating a new Swift file called Project.swift . This will be the model that represents a grouped collection of ToDo items.

Creating a new Model file is done by selecting File > New > File... and selecting "Swift File" from the file type selector panel. Press next then enter Project for the file name (the .swift extension will be added automatically) and navigate, if needed, to the Xcode project directory for SyncIntro and save the file.

Add the following code into the new file to complete the creation of the Project model.

Project.swift
import RealmSwift
class Project: Object {
@objc dynamic var projectId: String = UUID().uuidString
@objc dynamic var owner: String = ""
@objc dynamic var name: String = ""
@objc dynamic var timestamp: Date = Date()
let items = List<Item>()
override static func primaryKey() -> String? {
return "projectId"
}
}

Step 8: Adding a Project Lists View Controller

Now that we have a model to work with, we can create a view controller in which we can both create new projects or view/select existing ones.

As you did with the Project model , creating a new view controller is done by selecting File > New > File... and this time selecting "Cocoa Touch Class" from the file type selector panel. Press next then enter ProjectsViewController for the file name (the .swift extension will be added automatically) and navigate, if needed, to your Xcode project directory and save the file...

Before we add the entire contents needed for this file we will look at the relevant code fragments that make this unique to Query-based Synchronization:

Opening the Realm with Query-based sync:

let config = SyncUser.current?.configuration()
realm = try! Realm(configuration: config!)

Here we use the default synced Realm, which is provided via an automatic SyncConfiguration that can be accessed from the logged in SyncUser. The automatic sync configuration determines which sync server to contact based on the user that is currently logged in, and opts into the query-based Query-based synchronization mode. This mode tells the server not to download all of the Realm's data, but rather to only download the portion of the object graph that matches queries that the client has explicitly subscribed to.

Setting up the Sync Query:

ProjectsViewController.swift
projects = realm.objects(Project.self).filter("owner = %@", SyncUser.current!.identity!).sorted(byKeyPath: "timestamp", ascending: false)

Here the syntax we used with a fully-synced Realm and a partially-synced Realm is the same - the difference is with a with a fully synced Realm the data is already synchronized (or may be in the process of being downloaded). The query is selecting Project model records where the owner property matches the ID of the currently logged in user (SyncUser.current!.identity!), and these will be sorted in date order, newest first.

It's important to note that with Query-based sync, no data is synchronized from the server to the client until a subscription is created. In our application the subscription is created as we are preparing to display the data in the controller's viewDidLoad method as follows:

ProjectsViewController.swift
subscription = projects.subscribe(named: "my-projects")
// here you might show an activity spinner to indicate you
// are waiting for the subscription to be processed
subscriptionToken = subscription.observe(\.state, options: .initial) { state in
if state == .complete {
// here you might remove any activity spinner
}
}

The first line creates the subscription itself. You can have any number of subscriptions for different kinds of data depending on your application's requirements.

The second line observes changes to the state of the subscription, similar to how you may observe changes to the state of a Realm collection or object. This observer lets your application react to changes in the status of the subscription.

Add Instance Variables for the Realm and Projects

Now let's begin implementing the above concepts. Create these instance variables just after the line declaring the ProjectsViewController class:

ProjectsViewController.swift
import UIKit
import RealmSwift
class ProjectsViewController: UIViewController {
let realm: Realm
let projects: Results<Project>
var notificationToken: NotificationToken?
var subscriptionToken: NotificationToken?
var subscription: SyncSubscription<Project>!
var tableView = UITableView()
let activityIndicator = UIActivityIndicatorView()
//...
}

Initialize the instance variables in the class's constructor; copy this code snippet in place just after the instance variable declaration:

ProjectsViewController.swift
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
let config = SyncUser.current?.configuration()
realm = try! Realm(configuration: config!)
projects = realm.objects(Project.self).filter("owner = %@", SyncUser.current!.identity!).sorted(byKeyPath: "timestamp", ascending: false)
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

At initialization 1) we open and connect to the realm 2) create our query, which once subscribed to, will pull every Project in the realm database that is satisfied by our filter, which in this case is any project that belongs to the currently logged in user.

Testing what we have so far

We've added a lot to this application, but so far haven't had an opportunity to test and and see if the work we've done so far works. Let's add just a few more lines of code so that we can build and run and ensure we're still on track.

In our class file, right after the end of the init method you just added we will add to the existing viewDidLoad method. This method sets up the view controller just after it is initialized. We will add more setup instructions shortly, but for now, let's just create a button handler that will add a logout button. This will allow us to test authentication and ensure that we can connect and log in to our Realm Cloud instance. Once this is ready we can add the rest of the support for adding and managing Projects and ToDo items.

ProjectsViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Logout", style: .plain, target: self, action: #selector(rightBarButtonDidClick))
}

Once this is in place we need to add a method that actually performs the logout function. This is done with a button handler. Add the method below to your view controller class -- it can be anywhere in the file; we'd suggest adding it right after the closing brace of the viewDidLoad method:

ProjectsViewController.swift
@objc func rightBarButtonDidClick() {
let alertController = UIAlertController(title: "Logout", message: "", preferredStyle: .alert);
alertController.addAction(UIAlertAction(title: "Yes, Logout", style: .destructive, handler: {
alert -> Void in
SyncUser.current?.logOut()
self.navigationController?.setViewControllers([WelcomeViewController()], animated: true)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}

Now we can fire up the application and see if we can log in. In the Xcode toolbar press build and run the app in the simulator again. You'll see the app start up and you will be presented with an login dialog much like the the one shown below.

If you would like, you can open Realm Studio and view the registration of new users to the Realm Object Server in real time. This is done by connecting to your instance, and navigating to the 'Users' tab.

From here you can log in, and an account will be created on the server. Press the logout button to log out. You can repeat this and create as many accounts on your Realm Cloud instance as you like.

Even though we are now successfully logging in a user, connecting to the realm, and crafting a query, we will not see Realm Object Server data on the client device. Remember:

no data is synchronized from the server to the client until a subscription is created

Adding the subscription and notifications

Now we will create a subscription and a notification system to prompt us that it is time to display the newly received data in the UI. In our viewDidLoad method of ProjectsViewController.swift, add the following subscription:

ProjectsViewController.swift
override func viewDidLoad() {
//...
subscription = projects.subscribe(named: "my-projects")
//...
}

When this line executes, the data that satisfies the query we created earlier begins to sync with our local device. If we were to access the projects instance variable either before this subscription executes or immediately after, the variable would not contain any data. So we will add a subscription notification to alert our app when the synchronization is complete, and we will add a results notification to tell us about the changes that are made.

ProjectsViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Logout", style: .plain, target: self, action: #selector(rightBarButtonDidClick))
view.addSubview(activityIndicator)
activityIndicator.center = self.view.center
activityIndicator.color = .darkGray
activityIndicator.isHidden = false
activityIndicator.hidesWhenStopped = true
subscription = projects.subscribe(named: "my-projects")
activityIndicator.startAnimating()
subscriptionToken = subscription.observe(\.state, options: .initial) { state in
if state == .complete {
self.activityIndicator.stopAnimating()
} else {
print("Subscription State: \(state)")
}
}
notificationToken = projects.observe { [weak self] (changes) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}

On line 13 we update the UI with an AtivityIndicator. On line 14 we register a subscription notification. When the state of that subscription is .complete we remove the ActivityIndicator because our data is synced and ready. To access this data we have registered a results notification on projects. Any time we receive new data or edit data, we update a UITableView, which we will implement in the next step.

In the notificationToken the changes object is filled with fine-grained notifications about which Items were changed, including the indexes of insertions, deletions, modifications, etc.

We can use this fine-grained change information to tell our UITableView instance which rows to change with the beginUpdates and endUpdates methods. Now any modifications to the Project objects will trigger animations appropriately.

We've stored the return value of observe into the notificationToken . So long as the notificationToken exists -- which will be whenever the ProjectsViewController is on-screen -- then the observe handler will consistently be called.

If our view controller ever goes off screen (for example if we had other views in our application), we would want to deallocate this notification handler. The deinit for the UIViewController is a convenient place to add notificationToken?.invalidate() method which cleans up this observer for us. Add the following deinit method following after the init methods near the top of the file:

ProjectsViewController.swift
deinit {
notificationToken?.invalidate()
subscriptionToken?.invalidate()
activityIndicator.stopAnimating()
}

Add a UITableView

Earlier we created a UITableView instance variable:

let tableView = UITableView()

Next, we'll initialize the the rest of the view, adding in our UITableView and setting the view's title as well; to do this modify the viewDidLoad method you added earlier:

ProjectsViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
title = "My Projects"
//...
//ActivityIndicator
//...
view.addSubview(tableView)
tableView.frame = self.view.frame
tableView.delegate = self
tableView.dataSource = self
//...
// Subscription
// Notifications
//...
}

In order to actually process changes to the table view we will need to subscribe to the UITableViewDelegate and UITableViewDataSource protocols. We will add these to our ItemsViewControllerjust after the UIViewController subclass definition; the resulting declaration should look like this:

ProjectsViewController.swift
class ProjectsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource

By declaring these protocols, we must now make sure theProjectsViewController conforms to them.

Implementing UITableViewDataSource

Realm naturally works with UITableViewDataSource . With an ordered collection of Results<Item> we can always see the items in the order they were created. We will begin by implementing what each cell in the UITableView will look like in the cellForRowAt method.

Place this method after the end of the ViewDidLoad method:

ProjectsViewController.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
cell.selectionStyle = .none
let project = projects[indexPath.row]
cell.textLabel?.text = project.name
cell.detailTextLabel?.text = project.items.count > 0 ? "\(project.items.count) task(s)" : "No tasks"
return cell
}

We've set the textLabel to equal the project name. The cell.detailTextLabel will display the current number of ToDo items that are associated with the project.

Next, we need to tell UITableViewDataSource how many projects to render; add this method after the previous one:

ProjectsViewController.swift
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return projects.count
}

Implement UITableViewDelegate

When a user clicks on a row of the UITableView the method is called and we will navigate to the relevant Item. We will implement the ItemViewController in step 9.

ProjectsViewController.swift
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let project = projects[indexPath.row]
let itemsVC = ItemsViewController()
itemsVC.project = project
self.navigationController?.pushViewController(itemsVC, animated: true)
}

Implementing Add and Delete functions

We'll create a new button to allow the user to to add new items to the Project list. In the viewDidLoad method, just after the first button handler declaration for the logout button, add the following:

ProjectsViewController.swift
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addItemButtonDidClick))

Now we will implement the logic this button executes to create and insert new Projects into our database. Just like the Nickname auth in the WelcomeViewController, we will present a UIAlertController with a textField that will prompt for the name of the ToDo Project.

Add the code below after the closing brace of the viewDidLoad method:

ProjectsViewController.swift
@objc func addItemButtonDidClick() {
let alertController = UIAlertController(title: "Add New Project", message: "", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Save", style: .default, handler: {
alert -> Void in
let textField = alertController.textFields![0] as UITextField
let project = Project()
project.name = textField.text ?? ""
project.owner = SyncUser.current!.identity!
try! self.realm.write {
self.realm.add(project)
}
// do something with textField
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alertController.addTextField(configurationHandler: {(textField : UITextField!) -> Void in
textField.placeholder = "New Item Text"
})
self.present(alertController, animated: true, completion: nil)
}

For the delete feature we will add another method from the UITableViewDataSource protocol that will allow us to respond to swipes and allow deletion of Projects:

ProjectsViewController.swift
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
let project = projects[indexPath.row]
if project.items.count > 0 {
confirmDeleteProjectAndTasks(project: project)
} else {
deleteProject(project)
}
}

Then add the corresponding methods:

ProjectsViewController.swift
@objc func confirmDeleteProjectAndTasks(project: Project) {
let alertController = UIAlertController(title: "Delete \(project.name)?", message: "This will delete \(project.items.count) task(s)", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Yes, Delete \(project.name)", style: .destructive, handler: {
alert -> Void in
self.deleteProject(project)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func deleteProject(_ project:Project) {
try! realm.write {
realm.delete(project.items)
realm.delete(project)
}
}

Step 9: Add an ItemsViewController

Now that we have a way to create, display, and delete Projects, let's continue implementing the relationship between Projects and Items. Create a new ViewController called ItemsViewController.swift and import RealmSwift

As before, create the view controller by selecting File > New > File.. and then selecting "Cocoa Touch Class" from the file type selector panel. Press next, then enter ItemsViewController for the class name and UIViewController for the subclass name. Press next again, navigate to your Xcode project directory if necessary, and save the file. Add the following frameworks and class:

ItemsViewController.swift
import UIKit
import RealmSwift
class ItemsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var items: List<Item>?
var project: Project?
var notificationToken: NotificationToken?
var tableView = UITableView()
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.items = project?.items // get the list of items from the project
title = project?.name ?? "Unnamed Project"
view.addSubview(tableView)
tableView.frame = self.view.frame
self.tableView.delegate = self
self.tableView.dataSource = self
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addItemButtonDidClick))
let logoutButton = UIBarButtonItem(title: "Logout", style: .plain, target: self, action: #selector(logoutButtonDidClick))
navigationItem.rightBarButtonItems = [logoutButton, addButton]
notificationToken = items?.observe { [weak self] (changes) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}
deinit {
notificationToken?.invalidate()
}
@objc func addItemButtonDidClick() {
let alertController = UIAlertController(title: "Add Item", message: "", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Save", style: .default, handler: {
alert -> Void in
let textField = alertController.textFields![0] as UITextField
let item = Item()
item.body = textField.text ?? ""
try! self.project?.realm?.write {
self.project?.items.append(item)
}
// do something with textField
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alertController.addTextField(configurationHandler: {(textField : UITextField!) -> Void in
textField.placeholder = "New Item Text"
})
self.present(alertController, animated: true, completion: nil)
}
@objc func logoutButtonDidClick() {
let alertController = UIAlertController(title: "Logout", message: "", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Yes, Logout", style: .destructive, handler: {
alert -> Void in
SyncUser.current?.logOut()
self.navigationController?.setViewControllers([WelcomeViewController()], animated: true)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
cell.selectionStyle = .none
let item = items?[indexPath.row]
cell.textLabel?.text = item?.body
cell.accessoryType = item!.isDone ? UITableViewCell.AccessoryType.checkmark : UITableViewCell.AccessoryType.none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items?[indexPath.row]
try! self.project?.realm?.write {
item!.isDone = !(item!.isDone)
}
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
let item = items?[indexPath.row]
try! self.project?.realm?.write {
self.project?.realm?.delete(item!)
}
}
}

This class has many similarities to our ProjectsViewController. The UI shares the "Add" and "Logout" button and we continue to display results through a UITableView.

In the ProjectsViewController we accessed and wrote directly to a local instance of a realm. Now in the ItemsViewController, rather than add the new item to the Realm directly, we append a new item to the list of items managed by the Project that was passed to the view controller.

When we update the table view with a record we do the same thing - use the reference to the Realm that is held by the project the user selected --self.project?.realm.

Recap: What We've Done So Far...

You should now be able to build and run the application, log in and create projects and ToDo Items. If you would like, you can use our application as a reference:

With Query-based sync we selectively subscribe to Project records in order to get only the data we need, not all the possible data that may exist inside a Realm. Try logging in as different users to see that only the desired data will be synced.

Let's recap what we have done so far; in our Query-based sync powered ToDo app, we...

  1. Allow the user to log in with a nick name, and we present a ProjectsViewController.

  2. Allow the user to see a list of their existing projects, or to create one or more new projects that act a grouping mechanism (via the ProjectsViewController) for ToDo items. We know about (can find) these projects because of the ability to subscribe to Project records that match a specific query we are interested in. In this case projects that were created by our own user - represented by the SyncUser.current.identity which is Realm's way of tracking the currently logged in user.

  3. Have set up the ProjectsViewController in such a way that when we tap on the row for a given project, that project record is passed over to the ItemsViewController which uses this information to show us any existing ToDo items and/or create new ones that will be added to the selected project.

Step 10: Adding Permissions

So far we've used Query-based sync to synchronize only projects owned by the current user. However, nothing prevents a malicious user from seeing another user's projects by subscribing with a query that matches a broader set of Project instances.

To address this issue we will instead use Realm's permission system to limit access to each Project instance so only the user that created it can read, modify, or delete it.

A user's role

Permissions are assigned to a collection of zero or more users named a role. Since we want a given Project to be accessible only to a single user, we need to use a separate role for each user. We can then grant the current user's role the appropriate permissions when we create a new Project. This will have the effect of preventing all other users from seeing or modifying that Project instance.

By default, every logged-in user has a private role created for them. This role can be accessed at PermissionUser.role. We'll use this role when creating new projects.

Control access to Project instances

Next we add a permissions attribute to the Project model. This tells Realm that we wish to configure the permissions of each Project instance independently, and serves as the means for doing so.

Project.swift
class Project: Object {
// ...
let permissions = List<Permission>()
}

Note that we no longer need the owner attribute in our Project class as we're relying on the permission system, rather than queries, to limit access.

The Permission class represents the set of privileges we want to grant and the role to which they should be granted. Since we want to limit each Project to being visible to only a single user, we will grant the current user's role the appropriate permissions when we create a new Project. This will have the result of preventing all other users from seeing or modifying the new Project instance.

Set the permissions of a new Project

As we create a new Project we now associate a new Permission with it to restrict who has access to it.

ProjectsViewController.swift
let textField = alertController.textFields![0]
let project = Project()
project.name = textField.text ?? ""
try! self.realm.write {
self.realm.add(project)
let user = self.realm.object(ofType: PermissionUser.self, forPrimaryKey: SyncUser.current!.identity!)!
let permission = project.permissions.findOrCreate(forRole: user.role!)
permission.canRead = true
permission.canUpdate = true
permission.canDelete = true
}

By stating that the current user's role can read, update, and delete the Project, we prevent any other user from having access to the Project.

Lower the default permissions

By default Realm Cloud creates a set of permissive default permissions and roles. This allows getting started on developing your application without first having to worry about permissions and roles. However, these permissive default permissions should be tightened before deploying your application.

By default, all users are added to an everyone role, and this role has access to all objects within the Realm. As part of lowering the default permissions, we will limit the access of this everyone role.

In this demo, we lower the privileges programmatically the first time a user logs in to our ToDo application. In practice, you'll want to configure these privileges using Realm Studio or a script prior to your users running the application.

There are two levels of permissions that we will lower: class-level permissions, and Realm-level permissions.

Class-level permissions

We use permissions to limit querying to only the Project type. This means that the only Item objects that can be synchronized are those associated with a Project that we have permissions to access. Additionally, we remove the ability for everyone to change the permissions of each of our model classes.

// Ensure that class-level permissions cannot be modified by anyone but admin users.
// The Project type can be queried, while Item cannot. This means that the only Item
// objects that will be synchronized are those associated with our Projects.
// Additionally, we prevent roles from being modified to avoid malicious users
// from gaining access to other user's projects by adding themselves as members
// of that user's private role.
let queryable = [Project.className(): true, Item.className(): false, PermissionRole.className(): true]
let updateable = [Project.className(): true, Item.className(): true, PermissionRole.className(): false]
for cls in [Project.self, Item.self, PermissionRole.self] {
let everyonePermission = realm.permissions(forType: cls).findOrCreate(forRoleNamed: "everyone")
everyonePermission.canQuery = queryable[cls.className()]!
everyonePermission.canUpdate = updateable[cls.className()]!
everyonePermission.canSetPermissions = false
}

Realm-level permissions

We use permissions to prevent the schema and permissions from being modified. Note that the order in which we perform these operations is significant, as removing the ability to modify permissions would cause subsequent permission changes to be rejected.

// Ensure that the schema and Realm-level permissions cannot be modified by anyone but admin users.
let everyonePermission = realm.permissions.findOrCreate(forRoleNamed: "everyone")
everyonePermission.canModifySchema = false
// `canSetPermissions` must be disabled last, as it would otherwise prevent other permission changes
// from taking effect.
everyonePermission.canSetPermissions = false

Finally, to ensure that everything is working correctly we will need to edit our central query to no longer filter by the logged in user, as this will now occur automatically because of our new permissions.

ProjectsViewController.swift
projects = realm.objects(Project.self).sorted(byKeyPath: "timestamp", ascending: false)

Conclusion

Congratulations on making it this far! We believe many of the topics we've covered in this tutorial will aid you in your development. For next steps we recommend viewing further examples on several key points: