Docs Menu

iOS Swift Tutorial

On this page

In Part 1 of this tutorial, you will create a task tracker app that allows users to manage a personal list of tasks stored in Realm Database. Once you've completed the local version of the app, you can enhance your application in Part 2 with Realm Sync to:

  • Register users with email and password.
  • Sign users into accounts with email and password and sign out later.
  • View a list of team members in a user's project.
  • Add and remove team members to a user's project.
  • View all projects a user is a member of and contribute tasks to those projects.

Part 1 should take around 30 minutes to complete. The optional part 2 should take an additional 30 minutes.

Note
Check Out the Quick Start

If you prefer to explore on your own rather than follow a guided tutorial, check out the iOS Quick Start. It includes copyable code examples and the essential information that you need to set up a MongoDB Realm application.

Using SwiftUI and Combine? Check out Use Realm Database with SwiftUI and Combine.

Important
Prerequisites
  • Xcode version 12.2 or higher, which requires macOS 10.15.4 or higher.
  • Target of iOS 13.0.
1

We've already put together a task tracker iOS application that has most of the code you'll need. You can clone the client application repository directly from GitHub:

git clone --branch start https://github.com/mongodb-university/realm-tutorial-ios-swift.git
Tip

The start branch is an incomplete version of the app that we will complete in this tutorial. To view a local-only version of the app:

  1. Navigate to the root directory of the client application repository:

    cd realm-tutorial-ios-swift
  2. Check out the local branch:

    git checkout local
  3. Run the app by clicking the "Play" button in the upper-left corner of the Xcode window.
2

In the Xcode Project Navigator, you can see the source files of the task tracker application in the Task Tracker folder. The relevant files are as follows:

File
Purpose
Models.swift
Define the Realm object models used by this app.
SceneDelegate.swift
Part 2: Declare the global Realm app instance for the Realm Sync portion of the tutorial.
WelcomeViewController.swift
Implement the login and user registration functionality.
TasksViewController.swift
Create the list view for the tasks in a given project.
ProjectsViewController.swift
Display the user's list of projects. In the local app, there is only the local user's project. In Part 2, when we add Sync, we'll add the projects where the logged in user is a member.
ManageTeamViewController.swift
Part 2: Manage members of a user's project.
3

Navigate to the Models.swift file to implement the Realm Object Models used in this app. Realm object models derive from Object from the RealmSwift library, which allows us to store them in Realm Database. The Task class is currently just a normal Swift class. Let's turn it into a Realm object model:

class Task: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name: String = ""
@Persisted var owner: String?
@Persisted var status: String = ""
var statusEnum: TaskStatus {
get {
return TaskStatus(rawValue: status) ?? .Open
}
set {
status = newValue.rawValue
}
}
convenience init(name: String) {
self.init()
self.name = name
}
}
Note

To learn more about how Realm Object models are used in iOS applications, see Read & Write Data - iOS SDK in our iOS client guide.

4

Navigate to the ProjectsViewController.swift file. For the local application, the user only has their own project - and their own realm - to open. We'll specify the configuration to use when we open the realm in init(userRealmConfiguration: Realm.Configuration):

self.userRealm = try! Realm(configuration: userRealmConfiguration)
super.init(nibName: nil, bundle: nil)
5

Navigate to the TasksViewController.swift file, where we'll implement the list of Tasks in a Project. The TasksViewController class holds an array of Task objects. We already converted the Task class to a Realm object model. In order to hold a live collection of Realm objects contained in a realm, we need to use RealmResults instead of a standard Swift array. Let's convert that property from an array of Tasks ([Task]) to a RealmResults collection of Tasks (Results<Task>) now:

let tableView = UITableView()
let realm: Realm
var notificationToken: NotificationToken?
let tasks: Results<Task>

We can initialize the tasks property with a query on the project realm. Once we have the live tasks collection, we can observe that for changes:

required init(realmConfiguration: Realm.Configuration, title: String) {
self.realm = try! Realm(configuration: realmConfiguration)
// Access all tasks in the realm, sorted by _id so that the ordering is defined.
tasks = realm.objects(Task.self).sorted(byKeyPath: "_id")
super.init(nibName: nil, bundle: nil)
self.title = title
// Observe the tasks for changes. Hang on to the returned notification token.
notificationToken = tasks.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.performBatchUpdates({
// It's important to be sure to always update a table in this order:
// deletions, insertions, then updates. Otherwise, you could be unintentionally
// updating at the wrong index!
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
})
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}

Calls to observe Realm objects return a notificationToken. Retain the notificationToken as long as you want to continue observing. When done observing -- for example, because the user navigated away from this view -- be sure to invalidate the token. The deinit method is a good place to do this:

deinit {
// Always invalidate any notification tokens when you are done with them.
notificationToken?.invalidate()
}

The TasksViewController already populates the UI using the Tasks in the list. Check out the tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAt:) methods to see how the Realm object model version of the Task class and the RealmResults object are drop-in replacements for the regular class and array respectively. No changes are required in these methods.

The TasksViewController also wires up the Add button at the top of the view to the addButtonDidClick() method. We can implement the Task creation in this method:

// Create a new Task with the text that the user entered.
let task = Task(name: textField.text ?? "New Task")
// Any writes to the Realm must occur in a write block.
try! self.realm.write {
// Add the Task to the Realm. That's it!
self.realm.add(task)
}

When the user selects a Task in the list, we present them with an action sheet to allow them to update the Task's status. Complete the tableView(_:didSelectRowAt:) method implementation as follows:

// If the task is not in the Open state, we can set it to open. Otherwise, that action will not be available.
// We do this for the other two states -- InProgress and Complete.
if task.statusEnum != .Open {
actionSheet.addAction(UIAlertAction(title: "Open", style: .default) { _ in
// Any modifications to managed objects must occur in a write block.
// When we modify the Task's state, that change is automatically reflected in the realm.
try! self.realm.write {
task.statusEnum = .Open
}
})
}
if task.statusEnum != .InProgress {
actionSheet.addAction(UIAlertAction(title: "Start Progress", style: .default) { _ in
try! self.realm.write {
task.statusEnum = .InProgress
}
})
}
if task.statusEnum != .Complete {
actionSheet.addAction(UIAlertAction(title: "Complete", style: .default) { _ in
try! self.realm.write {
task.statusEnum = .Complete
}
})
}

To handle swipes to delete a Task, we implement the tableView(_:commit:forRowAt:) method:

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
// User can swipe to delete items.
let task = tasks[indexPath.row]
// All modifications to a realm must happen in a write block.
try! realm.write {
// Delete the Task.
realm.delete(task)
}
}
6

Finally, go to the WelcomeViewController and implement the signIn() method. Create a realm configuration that specifies which "project" to open and pass it to the TasksViewController:

@objc func signIn() {
// Go to the list of tasks in the user object contained in the user realm.
var config = Realm.Configuration.defaultConfiguration
// This configuration step is not really needed, but if we add Sync later,
// this allows us to keep the tasks we made.
config.fileURL!.deleteLastPathComponent()
config.fileURL!.appendPathComponent("project=\(self.username!)")
config.fileURL!.appendPathExtension("realm")
navigationController!.pushViewController(
TasksViewController(realmConfiguration: config, title: "\(username!)'s Tasks"),
animated: true
)
}
Note

TasksViewController works the same whether we're using Sync or not. That makes it easier to add Sync later in Part 2. We'll only need to change the configuration we pass into TasksViewController.

7

Once you have completed the code, you can run the app and check functionality.

Click the Run button in Xcode. If the app builds successfully, here are some things you can try in the app:

  • Create a user with username test
  • Navigate to "My Project"
  • Add, update, and remove some tasks
Tip

If something isn't working for you, you can check out the local branch of this repo to compare your code with our finished solution.

Important
Prerequisites
Tip

To view a complete synced version of the app:

  1. Navigate to the root directory of the client application repository:

    cd realm-tutorial-ios-swift
  2. Check out the sync branch:

    git checkout sync
  3. In SceneDelegate.swift, replace <your-realm-app-ID-here> with your Realm app ID, which you can find in the Realm UI.
  4. Run the app by clicking the "Play" button in the upper-left corner of the Xcode window.
1

To get the app working with your backend, you first need to add your Realm App ID to the SceneDelegate.swift file. Declare a global Realm App instance:

let app = App(id: "<your-realm-app-ID-here>")

Change the value of id to your Realm app ID, which you can find in the Realm UI.

2

To support online functionality like shared projects, we'll need two additional models.

Add an embedded object model for user projects:

class Project: EmbeddedObject {
@Persisted var name: String?
@Persisted var partition: String?
convenience init(partition: String, name: String) {
self.init()
self.partition = partition
self.name = name
}
}

And a model for custom user data that stores the list of projects as user can access:

class User: Object {
@Persisted(primaryKey: true) var _id: String = ""
@Persisted var name: String = ""
@Persisted var memberOf: List<Project>
}
3

Navigate to the WelcomeViewController.swift file, which is where we implement all login and user registration logic. This controller is set up with a text field for email and password, sign in and sign up buttons, and an activity indicator to show when the app is handling an authentication request. To enable users to log in with MongoDB Realm accounts, we'll add a field where the user can enter a password and a button to register an account as variables in WelcomeViewController:

let passwordField = UITextField()
let signInButton = UIButton(type: .roundedRect)
let signUpButton = UIButton(type: .roundedRect)

We'll also need to add an accessor to get the password entered by the user when they register or log in:

var password: String? {
get {
return passwordField.text
}
}

In the viewDidLoad() method, update the infoLabel value to mention both a username and password:

infoLabel.text = "Please enter an email and password."

Change the placeholder value of the username entry field to "Email", since usernames for MongoDB Realm accounts should be email addresses:

// Configure the username text input field.
usernameField.placeholder = "Email"

And configure the password entry field with placeholder text and secure text entry:

// Configure the password text input field.
passwordField.placeholder = "Password"
passwordField.isSecureTextEntry = true
passwordField.borderStyle = .roundedRect
container.addArrangedSubview(passwordField)

Configure a sign-up button that users can click to register an account:

// Configure the sign up button.
signUpButton.setTitle("Sign Up", for: .normal)
signUpButton.addTarget(self, action: #selector(signUp), for: .touchUpInside)
container.addArrangedSubview(signUpButton)

Next, in the setLoading() method, enable the password field alongside the username field when the view loads:

usernameField.isEnabled = !loading
passwordField.isEnabled = !loading
signInButton.isEnabled = !loading
signUpButton.isEnabled = !loading

Implement the signUp() method to register a new user, which uses the email/password authentication provider of the Realm app to register a new user:

@objc func signUp() {
setLoading(true)
app.emailPasswordAuth.registerUser(email: username!, password: password!, completion: { [weak self](error) in
// Completion handlers are not necessarily called on the UI thread.
// This call to DispatchQueue.main.async ensures that any changes to the UI,
// namely disabling the loading indicator and navigating to the next page,
// are handled on the UI thread:
DispatchQueue.main.async {
self!.setLoading(false)
guard error == nil else {
print("Signup failed: \(error!)")
self!.errorLabel.text = "Signup failed: \(error!.localizedDescription)"
return
}
print("Signup successful!")
// Registering just registers. Now we need to sign in, but we can reuse the existing email and password.
self!.errorLabel.text = "Signup successful! Signing in..."
self!.signIn()
}
})
}

Finally, implement the signIn() method to authenticate user credentials with your backend Realm app using email/password credentials. Once logged in successfully, open the user realm and navigate to the ProjectsViewController. We open the realm using asyncOpen() because it fully downloads any remote data before proceeding:

@objc func signIn() {
print("Log in as user: \(username!)")
setLoading(true)
app.login(credentials: Credentials.emailPassword(email: username!, password: password!)) { [weak self](result) in
// Completion handlers are not necessarily called on the UI thread.
// This call to DispatchQueue.main.async ensures that any changes to the UI,
// namely disabling the loading indicator and navigating to the next page,
// are handled on the UI thread:
DispatchQueue.main.async {
self!.setLoading(false)
switch result {
case .failure(let error):
// Auth error: user already exists? Try logging in as that user.
print("Login failed: \(error)")
self!.errorLabel.text = "Login failed: \(error.localizedDescription)"
return
case .success(let user):
print("Login succeeded!")
// Load again while we open the realm.
self!.setLoading(true)
// Get a configuration to open the synced realm.
let configuration = user.configuration(partitionValue: "user=\(user.id)")
// Open the realm asynchronously so that it downloads the remote copy before
// opening the local copy.
Realm.asyncOpen(configuration: configuration) { [weak self](result) in
DispatchQueue.main.async {
self!.setLoading(false)
switch result {
case .failure(let error):
fatalError("Failed to open realm: \(error)")
case .success:
// Go to the list of projects in the user object contained in the user realm.
self!.navigationController!.pushViewController(ProjectsViewController(userRealmConfiguration: configuration), animated: true)
}
}
}
}
}
}
}
4

Open the ProjectsViewController.swift file, which is where we present the user with a list of projects they are a member of.

Let's provide a way for a user to log out and get back to the WelcomeViewController. The viewDidLoad() method hooks up the Log Out button at the top of the view to the logOutButtonDidClick() method. We can implement logOutButtonDidClick() as follows:

@objc func logOutButtonDidClick() {
let alertController = UIAlertController(title: "Log Out", message: "", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Yes, Log Out", style: .destructive, handler: {
_ -> Void in
print("Logging out...")
self.navigationController?.popViewController(animated: true)
app.currentUser?.logOut { (_) in
DispatchQueue.main.async {
print("Logged out!")
self.navigationController?.popViewController(animated: true)
}
}
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}

Next, open the realm with the configuration passed in from the previous controller. By receiving the configuration rather than creating one here, the ViewController does not need to know whether the realm is using Sync or not.

Add a change listener at the end of the init method that watches for a new User object in the user realm. Because your backend app creates User objects with a Trigger, it can sometimes take a few seconds after account creation for the backend Trigger to generate a user's User object.

self.userRealm = try! Realm(configuration: userRealmConfiguration)
super.init(nibName: nil, bundle: nil)
// There should only be one user in my realm - that is myself
let usersInRealm = userRealm.objects(User.self)
notificationToken = usersInRealm.observe { [weak self, usersInRealm] (_) in
self?.userData = usersInRealm.first
guard let tableView = self?.tableView else { return }
tableView.reloadData()
}

The ProjectsViewController reads the list of projects the user has access to from a custom user data object. We added the model for the user custom data earlier in this tutorial.

Note
How Do We Know Which Projects a User Can Access?

The backend you imported makes exactly one custom user data object for each user upon signup. This custom user data object contains a list of partitions a user can read and a list of partitions a user can write to.

The backend is set up so that every user has read-only access to their own custom user data object. The backend also has functions to add and remove access to projects, which we will use later when we add the Manage Team view.

By managing the custom user data object entirely on the backend and only providing read-only access on the client side, we prevent a malicious client from granting themselves arbitrary permissions.

Now add a deinit method to ensure that your application invalidates the notification token for that change listener when the view is destroyed:

deinit {
// Always invalidate any notification tokens when you are done with them.
notificationToken?.invalidate()
}

Since the ProjectsViewController implements the UITableViewDelegate protocol for its own list, let's implement these methods. First, implement the tableView(_:numberOfRowsInSection:) method to return the number of available projects to the current user. Use the count of projects that the user can access:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// You always have at least one project (your own)
return userData?.memberOf.count ?? 1
}

Next, implement the tableView(_:cellForRowAt:) to fill out the project information for each cell:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
cell.selectionStyle = .none
// User data may not have loaded yet. You always have your own project.
let projectName = userData?.memberOf[indexPath.row].name ?? "My Project"
cell.textLabel?.text = projectName
return cell
}

Implement the tableView(_:didSelectRowAt:) method to handle what happens when the user clicks a project in the list. We'll open the project realm before navigating to the TasksViewController so that if anything goes wrong, we can handle the error before launching a separate view:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let user = app.currentUser!
let project = userData?.memberOf[indexPath.row] ?? Project(partition: "project=\(user.id)", name: "My Project")
let configuration = user.configuration(partitionValue: project.partition!)
Realm.asyncOpen(configuration: configuration) { [weak self] (result) in
switch result {
case .failure(let error):
fatalError("Failed to open realm: \(error)")
case .success(let realm):
self?.navigationController?.pushViewController(
TasksViewController(realmConfiguration: configuration, title: "\(project.name!)'s Tasks"),
animated: true
)
}
}
}
5

Next, we'll add a menu to manage members of a project. You can open this menu with a button. Navigate to TaskViewController.swift. Add a new method that checks if a project is owned by the current user. This controls whether or not the user can manage the list of users allowed to access that project:

// Returns true if these are the user's own tasks.
func isOwnTasks() -> Bool {
let partitionValue = self.realm.configuration.syncConfiguration?.partitionValue?.stringValue
return partitionValue != nil && partitionValue == "project=\(app.currentUser!.id)"
}

Add the corresponding click handler for that method:

@objc func manageTeamButtonDidClick() {
present(UINavigationController(rootViewController: ManageTeamViewController()), animated: true)
}

At the end of the viewDidLoad method, add logic that calls the method you just created:

if isOwnTasks() {
// Only set up the manage team button if these are tasks the user owns.
toolbarItems = [
UIBarButtonItem(title: "Manage Team", style: .plain, target: self, action: #selector(manageTeamButtonDidClick))
]
navigationController?.isToolbarHidden = false
}
Note
ManageTeamViewController Error

The ManageTeamViewController doesn't exist yet, so if you see an error about it here, it should go away after the next step.

6

A user can add and remove team members to their own Project using the Manage Team view. Since the client side cannot handle access management, we need to call out to our Realm functions we defined earlier.

Warning

This view heavily relies on the serverless functions we built into the backend. If you did not import the backend, the code in this section will not work as expected.

Navigate to the ManageTeamViewController.swift file, which defines the view that pops up when a user clicks the "Manage Team" action on the TasksViewController. The file should be empty except for a copyright disclaimer comment. Copy and paste the following code into the file:

import UIKit
import RealmSwift
class ManageTeamViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let tableView = UITableView()
var activityIndicator = UIActivityIndicatorView(style: .large)
var members: [Member] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "My Team"
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(closeButtonDidClick))
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonDidClick))
tableView.dataSource = self
tableView.delegate = self
tableView.frame = self.view.frame
view.addSubview(tableView)
activityIndicator.center = view.center
view.addSubview(activityIndicator)
fetchTeamMembers()
}
@objc func closeButtonDidClick() {
presentingViewController!.dismiss(animated: true)
}
@objc func addButtonDidClick() {
let alertController = UIAlertController(title: "Add Team Member", message: "Enter your team member's email address.", preferredStyle: .alert)
// When the user clicks the add button, present them with a dialog to enter the member's email address.
alertController.addAction(UIAlertAction(title: "Add", style: .default, handler: {
[weak self] _ -> Void in
let textField = alertController.textFields![0]
self!.addTeamMember(email: textField.text!)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alertController.addTextField(configurationHandler: { (textField: UITextField!) -> Void in
textField.placeholder = "someone@example.com"
})
// Show the dialog.
self.present(alertController, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return members.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let member = members[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
cell.selectionStyle = .none
cell.textLabel?.text = member.name
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
removeTeamMember(email: members[indexPath.row].name)
}
// Calls a Realm function to fetch the team members and adds them to the list
func fetchTeamMembers() {
// Start loading indicator
activityIndicator.startAnimating()
let user = app.currentUser!
user.functions.getMyTeamMembers([]) { [weak self](result, error) in
DispatchQueue.main.async {
guard self != nil else {
// This can happen if the view is dismissed
// before the operation completes
print("Team members list no longer needed.")
return
}
// Stop loading indicator
self!.activityIndicator.stopAnimating()
guard error == nil else {
print("Fetch team members failed: \(error!.localizedDescription)")
return
}
print("Fetch team members complete.")
// Convert documents to members array
self!.members = result!.arrayValue!.map({ (bson) in
return Member(document: bson!.documentValue!)
})
// Notify UI of changed data
self!.tableView.reloadData()
}
}
}
func addTeamMember(email: String) {
print("Adding member: \(email)")
activityIndicator.startAnimating()
let user = app.currentUser!
user.functions.addTeamMember([AnyBSON(email)], self.onTeamMemberOperationComplete)
}
func removeTeamMember(email: String) {
print("Removing member: \(email)")
activityIndicator.startAnimating()
let user = app.currentUser!
user.functions.removeTeamMember([AnyBSON(email)], self.onTeamMemberOperationComplete)
}
private func onTeamMemberOperationComplete(result: AnyBSON?, realmError: Error?) {
DispatchQueue.main.async { [self] in
// Always be sure to stop the activity indicator
activityIndicator.stopAnimating()
// There are two kinds of errors:
// - The Realm function call itself failed (for example, due to network error)
// - The Realm function call succeeded, but our business logic within the function returned an error,
// (for example, user is not a member of the team).
var errorMessage: String?
if realmError != nil {
// Error from Realm (failed function call, network error...)
errorMessage = realmError!.localizedDescription
} else if let resultDocument = result?.documentValue {
// Check for user error. The addTeamMember function we defined returns an object
// with the `error` field set if there was a user error.
errorMessage = resultDocument["error"]??.stringValue
} else {
// The function call did not fail but the result was not a document.
// This is unexpected.
errorMessage = "Unexpected result returned from server"
}
// Present error message if any
guard errorMessage == nil else {
print("Team operation failed: \(errorMessage!)")
let alertController = UIAlertController(
title: "Error",
message: errorMessage!,
preferredStyle: .alert
)
alertController.addAction(UIAlertAction(title: "OK", style: .cancel))
present(alertController, animated: true)
return
}
// Otherwise, fetch new team members list
print("Team operation successful")
fetchTeamMembers()
}
}
}

You should now have a fully functional ManageTeamViewController implementation. However, it's worth taking a look at some of the core logic to get a sense of how team management works in Task Tracker.

The ManageTeamViewController uses fetchTeamMembers() to get the list of team members, which calls the getMyTeamMembers Realm function to access a list of users with access to the project:

// Calls a Realm function to fetch the team members and adds them to the list
func fetchTeamMembers() {
// Start loading indicator
activityIndicator.startAnimating()
let user = app.currentUser!
user.functions.getMyTeamMembers([]) { [weak self](result, error) in
DispatchQueue.main.async {
guard self != nil else {
// This can happen if the view is dismissed
// before the operation completes
print("Team members list no longer needed.")
return
}
// Stop loading indicator
self!.activityIndicator.stopAnimating()
guard error == nil else {
print("Fetch team members failed: \(error!.localizedDescription)")
return
}
print("Fetch team members complete.")
// Convert documents to members array
self!.members = result!.arrayValue!.map({ (bson) in
return Member(document: bson!.documentValue!)
})
// Notify UI of changed data
self!.tableView.reloadData()
}
}
}

The ManageTeamViewController wires up the add button and swipe to delete functionality to the addTeamMember() and removeTeamMember() methods, respectively.

The addTeamMember() method calls the addTeamMember Realm function and can use the onTeamMemberOperationComplete() method as a completion handler:

func addTeamMember(email: String) {
print("Adding member: \(email)")
activityIndicator.startAnimating()
let user = app.currentUser!
user.functions.addTeamMember([AnyBSON(email)], self.onTeamMemberOperationComplete)
}

The removeTeamMember() method calls the removeTeamMember Realm function and also uses the onTeamMemberOperationComplete() method as a completion handler:

func removeTeamMember(email: String) {
print("Removing member: \(email)")
activityIndicator.startAnimating()
let user = app.currentUser!
user.functions.removeTeamMember([AnyBSON(email)], self.onTeamMemberOperationComplete)
}

The onTeamMemberOperationComplete() method presents any errors to the user and refreshes the member list.

7

Once you have completed the code, you can run the app and check functionality.

Click the Run button in Xcode. If the app builds successfully, here are some things you can try in the app:

  • Create a user with email first@example.com
  • Explore the app, then log out or launch a second instance of the app on another device or simulator
  • Create another user with email second@example.com
  • Navigate to second@example.com's project
  • Add, update, and remove some tasks
  • Click "Manage Team"
  • Add first@example.com to your team
  • Log out and log in as first@example.com
  • See two projects in the projects list
  • Navigate to second@example.com's project
  • Collaborate by adding, updating, and removing some new tasks
Tip

If something isn't working for you, you can check out the sync branch of this repo to compare your code with our finished solution.

Note
Leave Feedback

How did it go? Please let us know if this tutorial was helpful or if you had any issues by using the feedback widget on the right side of the page.

Give Feedback
© 2021 MongoDB, Inc.

About

  • Careers
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2021 MongoDB, Inc.