Query-based Sync is not recommended. For applications using Realm Sync, we recommend Full Sync. Learn more about our plans for the future of Realm Sync here.
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.
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.
Enabled nickname authentication provider: this project uses the deprecated nickname authentication provider, which you must opt-in to use. To opt in,
Log in to https://cloud.realm.io.
Select your instance.
Go to the "Settings" tab.
Under "Authentication", click to expand "Nickname".
Click "Enable" and accept the prompt.
If you forget to enable nickname authentication and run the app anyway, you will see the following fatal error: "Your request parameters did not validate. provider: Invalid parameter 'provider'!".
Please note that the nickname authentication provider is only intended for development purposes. If you intend to use the same instance for production, you should disable the nickname authentication provider after you are done with this sample app.
This tutorial uses CocoaPods to install the required Realm frameworks. You can install CocoaPods by following the installation instructions at cocoapods.org.
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.
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
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.
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:
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.
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.
Open the Info.plist
for your target (this will be in the iOSToDoApp group in the file navigator)
Find the Main storyboard file base name
entry and remove it by pressing on the -
icon next to it in the property editor
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.swiftfunc 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}
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 legacy 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.Swiftimport Foundationstruct 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 thisstatic let AUTH_URL = URL(string: "https://\(MY_INSTANCE_ADDRESS)")!static let REALM_URL = URL(string: "realms://\(MY_INSTANCE_ADDRESS)/ToDo")!}
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.swiftoverride 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 inlet textField = alertController.textFields![0] as UITextFieldlet creds = SyncCredentials.nickname(textField.text!)SyncUser.logIn(with: creds, server: Constants.AUTH_URL, onCompletion: { [weak self](user, err) inif 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 intextField.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.
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.swiftimport RealmSwiftclass 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.
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. Pressnext
then enterProject
for the file name (the.swift
extension will be added automatically) and navigate, if needed, to the Xcode project directory forSyncIntro
and save the file.
Add the following code into the new file to complete the creation of the Project model.
Project.swiftimport RealmSwiftclass 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"}}
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 selectingFile > New > File..
. and this time selecting "Cocoa Touch Class" from the file type selector panel. Pressnext
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:
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.
ProjectsViewController.swiftprojects = 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.swiftsubscription = projects.subscribe(named: "my-projects")// here you might show an activity spinner to indicate you// are waiting for the subscription to be processedsubscriptionToken = subscription.observe(\.state, options: .initial) { state inif 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.
Now let's begin implementing the above concepts. Create these instance variables just after the line declaring the ProjectsViewController
class:
ProjectsViewController.swiftimport UIKitimport RealmSwiftclass ProjectsViewController: UIViewController {let realm: Realmlet 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.swiftoverride 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.
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.swiftoverride 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 inSyncUser.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
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.swiftoverride 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.swiftoverride func viewDidLoad() {super.viewDidLoad()navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Logout", style: .plain, target: self, action: #selector(rightBarButtonDidClick))view.addSubview(activityIndicator)activityIndicator.center = self.view.centeractivityIndicator.color = .darkGrayactivityIndicator.isHidden = falseactivityIndicator.hidesWhenStopped = truesubscription = projects.subscribe(named: "my-projects")activityIndicator.startAnimating()subscriptionToken = subscription.observe(\.state, options: .initial) { state inif state == .complete {self.activityIndicator.stopAnimating()} else {print("Subscription State: \(state)")}}notificationToken = projects.observe { [weak self] (changes) inguard let tableView = self?.tableView else { return }switch changes {case .initial:// Results are now populated and can be accessed without blocking the UItableView.reloadData()case .update(_, let deletions, let insertions, let modifications):// Query results have changed, so apply them to the UITableViewtableView.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 threadfatalError("\(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.swiftdeinit {notificationToken?.invalidate()subscriptionToken?.invalidate()activityIndicator.stopAnimating()}
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.swiftoverride func viewDidLoad() {super.viewDidLoad()title = "My Projects"//...//ActivityIndicator//...view.addSubview(tableView)tableView.frame = self.view.frametableView.delegate = selftableView.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 ItemsViewController
just after the UIViewController
subclass definition; the resulting declaration should look like this:
ProjectsViewController.swiftclass ProjectsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource
By declaring these protocols, we must now make sure theProjectsViewController
conforms to them.
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.swiftfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")cell.selectionStyle = .nonelet project = projects[indexPath.row]cell.textLabel?.text = project.namecell.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.swiftfunc tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {return projects.count}
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.swiftfunc tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {let project = projects[indexPath.row]let itemsVC = ItemsViewController()itemsVC.project = projectself.navigationController?.pushViewController(itemsVC, animated: true)}
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.swiftnavigationItem.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 inlet textField = alertController.textFields![0] as UITextFieldlet 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 intextField.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.swiftfunc 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 inself.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)}}
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.swiftimport UIKitimport RealmSwiftclass 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 projecttitle = project?.name ?? "Unnamed Project"view.addSubview(tableView)tableView.frame = self.view.frameself.tableView.delegate = selfself.tableView.dataSource = selflet 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) inguard let tableView = self?.tableView else { return }switch changes {case .initial:// Results are now populated and can be accessed without blocking the UItableView.reloadData()case .update(_, let deletions, let insertions, let modifications):// Query results have changed, so apply them to the UITableViewtableView.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 threadfatalError("\(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 inlet textField = alertController.textFields![0] as UITextFieldlet 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 intextField.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 inSyncUser.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 = .nonelet item = items?[indexPath.row]cell.textLabel?.text = item?.bodycell.accessoryType = item!.isDone ? UITableViewCell.AccessoryType.checkmark : UITableViewCell.AccessoryType.nonereturn 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
.
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...
Allow the user to log in with a nick name, and we present a ProjectsViewController.
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.
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.
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.
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.
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.swiftclass 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.
As we create a new Project
we now associate a new Permission
with it to restrict who has access to it.
ProjectsViewController.swiftlet 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 = truepermission.canUpdate = truepermission.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
.
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.
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}
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.swiftprojects = realm.objects(Project.self).sorted(byKeyPath: "timestamp", ascending: false)
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:
First we looked at authentication.
Next we configured our realm.
Then we started syncing data.
Finally we added permissions.