iOS Swift Tutorial
On this page
- Overview
- Part 1: Set up the Mobile App
- Procedure
- Clone the Client App Repository
- Explore the App Structure
- Define the Object Models
- Specify the Realm to Open
- Implement the Tasks List
- Enable Sign-in
- Run and Test
- Part 2: Add Sync to the Mobile App
- Procedure
- Connect to Your MongoDB Realm App
- Update Your App's Realm Models
- Enable Authentication
- Implement the Projects List
- Add Project Member Management Button to the Tasks View
- Implement the Manage Team View
- Run and Test
- What's Next?
Overview
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.
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 Realm Database with SwiftUI QuickStart.
Part 1: Set up the Mobile App
- Xcode version 12.4 or higher, which requires macOS 10.15.4 or higher.
- Target of iOS 13.0.
Prefer to learn by watching? Follow along while we we complete Part 1 in this video tutorial walk-through!
Procedure
Clone the Client App Repository
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
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:
Navigate to the root directory of the client application repository:
cd realm-tutorial-ios-swift Check out the
local
branch:git checkout local - Run the app by clicking the "Play" button in the upper-left corner of the Xcode window.
Explore the App Structure
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. |
Define the Object Models
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 { true) var _id: ObjectId (primaryKey: var name: String = "" var owner: String? 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 } }
To learn more about how Realm Object models are used in iOS applications, see Read & Write Data - Swift SDK in our iOS client guide.
Specify the Realm to Open
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)
Implement the Tasks List
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) } }
Enable Sign-in
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 ) }
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.
Run and Test
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
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.
Part 2: Add Sync to the Mobile App
- Set up the backend
- Completed Part 1.
To view a complete synced version of the app:
Navigate to the root directory of the client application repository:
cd realm-tutorial-ios-swift Check out the
sync
branch:git checkout sync - In SceneDelegate.swift, replace
<your-realm-app-ID-here>
with your Realm app ID, which you can find in the Realm UI. - Run the app by clicking the "Play" button in the upper-left corner of the Xcode window.
Prefer to learn by watching? Follow along while we complete Part 2 in this video tutorial walk-through!
Procedure
Connect to Your MongoDB Realm App
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.
Update Your App's Realm Models
To support online functionality like shared projects, we'll need two additional models.
Add an embedded object model for user projects:
class Project: EmbeddedObject { var name: String? 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 { true) var _id: String = "" (primaryKey: var name: String = "" var memberOf: List<Project> }
Enable Authentication
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) } } } } } } }
Implement the Projects List
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.
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 ) } } }
Add Project Member Management Button to the Tasks View
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 }
The ManageTeamViewController doesn't exist yet, so if you see an error about it here, it should go away after the next step.
Implement the Manage Team View
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.
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.
Run and Test
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
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.
What's Next?
- Read our Swift SDK documentation.
- Try the MongoDB Realm Backend tutorial.
- Find developer-oriented blog posts and integration tutorials on the MongoDB Developer Hub.
- Join the MongoDB Community forum to learn from other MongoDB developers and technical experts.
How did it go? Use the Give Feedback tab at the bottom right of the page to let us know if this tutorial was helpful or if you had any issues.