HomeLearnHow-to

Adapting Apple's Scrumdinger SwiftUI Tutorial App to Use Realm

Published: Apr 09, 2021

  • Realm
  • Mobile
  • Swift
  • ...

By Andrew Morgan

Share

Apple published a great tutorial to teach developers how to create iOS apps using SwiftUI. I particularly like it because it doesn't make any assumptions about existing UIKit experience, making it ideal for developers new to iOS. That tutorial is built around an app named "Scrumdinger," which is designed to facilitate daily scrum meetings.

Apple's Scrumdinger implementation saves the app data to a local file whenever the user minimizes the app, and loads it again when they open the app. It seemed an interesting exercise to modify Scrumdinger to use Realm rather than a flat file to persist the data. This article steps through what changes were required to rebase Scrumdinger onto Realm.

An immediate benefit of the move is that changes are now persisted immediately, so nothing is lost if the device or app crashes. It's beyond the scope of this article, but now that the app data is stored in Realm, it would be straightforward to add enhancements such as:

  • Search meeting minutes for a string.
  • Filter minutes by date or attendees.
  • Sync data so that the same user can see all of their data on multiple iOS (and optionally, Android) devices.
  • Use Realm Sync Partitions to share scrum data between team members.
  • Sync the data to MongoDB Atlas so that it can be accessed by web apps or through a GraphQL API

#Prerequisites

  • Mac (sorry Windows and Linux users).
  • Xcode 12.4+.

I strongly recommend that you at least scan Apple's tutorial. I don't explain any of the existing app structure or code in this article.

#Adding Realm to the Scrumdinger App

First of all, a couple of notes about the GitHub repo for this project:

  • The main branch is the app as it appears in Apple's tutorial. This is the starting point for this article.
  • The realm branch contains a modified version of the Scrumdinger app that persists the application data in Realm. This is the finishing point for this article.
  • You can view the diff between the main and realm branches to see the changes needed to make the app run on Realm.

#Install and Run the Original Scrumdinger App

1git clone https://github.com/realm/Scrumdinger.git
2cd Scrumdinger
3open Scrumdinger.xcodeproj

From Xcode, select a simulator:

Selecting the iPhone 12 simulator in Xcode
Select an iOS simulator in Xcode.

Build and run the app with ⌘r:

Screen captiure from the Scrumdinger app, showing the 3 built-in scrums as colored tiles
Scrumdinger screen capture

Create a new daily scrum. Force close and restart the app with ⌘r. Note that your new scrum has been lost 😢. Don't worry, that's automatically fixed once we've migrated to Realm.

#Add the Realm SDK to the Project

To use Realm, we need to add the Realm-Cocoa SDK to the Scrumdinger Xcode project using the Swift Package Manager. Select the "Scrumdinger" project and the "Swift Packages" tab, and then click the "+" button:

Adding a new Swift Package Manager

Paste in https://github.com/realm/realm-cocoa as the package repository URL:

Adding a new Swift Package Manager

Add the RealmSwift package to the Scrumdinger target:

Adding a new Swift Package Manager

We can then start using the Realm SDK with import RealmSwift.

#Update Model Classes to be Realm Objects

To store an object in Realm, its class must inherit from Realm's Object class. If the class contains sub-classes, those classes must conform to Realm's EmbeddedObject protocol.

#Color

As with the original app's flat file, Realm can't natively persist the SwiftUI Color class, and so colors need to be stored as components. To that end, we need a Components class. It conforms to EmbeddedObject so that it can be embedded in a higher-level Realm Object class. The Objective-C code in the Realm SDK needs to react to changes within the class, and so it's annotated with @objcMembers, and the attributes are made dynamic:

1import RealmSwift
2
3@objcMembers class Components: EmbeddedObject {
4 dynamic var red: Double = 0
5 dynamic var green: Double = 0
6 dynamic var blue: Double = 0
7 dynamic var alpha: Double = 0
8
9 convenience init(red: Double, green: Double, blue: Double, alpha: Double) {
10 self.init()
11 self.red = red
12 self.green = green
13 self.blue = blue
14 self.alpha = alpha
15 }
16}

#DailyScrum

DailyScrum is converted from a struct to an Object class so that it can be persisted in Realm. By conforming to ObjectKeyIdentifiable, lists of DailyScrum objects can be used within SwiftUI ForEach views, with Realm managing the id identifier for each instance.

We use the Realm List class to store arrays.

1import RealmSwift
2
3@objcMembers class DailyScrum: Object, ObjectKeyIdentifiable {
4 dynamic var title = ""
5 var attendeeList = RealmSwift.List<String>()
6 dynamic var lengthInMinutes = 0
7 dynamic var colorComponents: Components?
8 var historyList = RealmSwift.List<History>()
9
10 var color: Color { Color(colorComponents ?? Components()) }
11 var attendees: [String] { Array(attendeeList) }
12 var history: [History] { Array(historyList) }
13
14 convenience init(title: String, attendees: [String], lengthInMinutes: Int, color: Color, history: [History] = []) {
15 self.init()
16 self.title = title
17 attendeeList.append(objectsIn: attendees)
18 self.lengthInMinutes = lengthInMinutes
19 self.colorComponents = color.components
20 for entry in history {
21 self.historyList.insert(entry, at: 0)
22 }
23 }
24}
25
26extension DailyScrum {
27 struct Data {
28 var title: String = ""
29 var attendees: [String] = []
30 var lengthInMinutes: Double = 5.0
31 var color: Color = .random
32 }
33
34 var data: Data {
35 return Data(title: title, attendees: attendees, lengthInMinutes: Double(lengthInMinutes), color: color)
36 }
37
38 func update(from data: Data) {
39 title = data.title
40 for attendee in data.attendees {
41 if !attendees.contains(attendee) {
42 self.attendeeList.append(attendee)
43 }
44 }
45 lengthInMinutes = Int(data.lengthInMinutes)
46 colorComponents = data.color.components
47 }
48}

#History

The History struct is replaced with a Realm Object class:

1import RealmSwift
2
3@objcMembers class History: EmbeddedObject, ObjectKeyIdentifiable {
4 dynamic var date: Date?
5 var attendeeList = List<String>()
6 dynamic var lengthInMinutes: Int = 0
7 dynamic var transcript: String?
8 var attendees: [String] { Array(attendeeList) }
9
10 convenience init(date: Date = Date(), attendees: [String], lengthInMinutes: Int, transcript: String? = nil) {
11 self.init()
12 self.date = date
13 attendeeList.append(objectsIn: attendees)
14 self.lengthInMinutes = lengthInMinutes
15 self.transcript = transcript
16 }
17}

#ScrumData

The ScrumData ObservableObject class was used to manage the copying of scrum data between the in-memory copy and a local iOS file (including serialization and deserialization). This is now handled automatically by Realm, and so this class can be deleted.

Nothing feels better than deleting boiler-plate code!

#Top-Level SwiftUI App

Once the data is being stored in Realm, there's no need for lifecycle code to load data when the app starts or save it when it's minimized, and so ScrumdingerApp becomes a simple wrapper for the top-level view (ScrumsView):

1import SwiftUI
2
3@main
4struct ScrumdingerApp: App {
5 var body: some Scene {
6 WindowGroup {
7 NavigationView {
8 ScrumsView()
9 }
10 }
11 }
12}

#SwiftUI Views

#ScrumsView

The move from a file to Realm simplifies the top-level view.

1import RealmSwift
2
3struct ScrumsView: View {
4 @ObservedResults(DailyScrum.self) var scrums
5 @State private var isPresented = false
6 @State private var newScrumData = DailyScrum.Data()
7 @State private var currentScrum = DailyScrum()
8
9 var body: some View {
10 List {
11 if let scrums = scrums {
12 ForEach(scrums) { scrum in
13 NavigationLink(destination: DetailView(scrum: scrum)) {
14 CardView(scrum: scrum)
15 }
16 .listRowBackground(scrum.color)
17 }
18 }
19 }
20 .navigationTitle("Daily Scrums")
21 .navigationBarItems(trailing: Button(action: {
22 isPresented = true
23 }) {
24 Image(systemName: "plus")
25 })
26 .sheet(isPresented: $isPresented) {
27 NavigationView {
28 EditView(scrumData: $newScrumData)
29 .navigationBarItems(leading: Button("Dismiss") {
30 isPresented = false
31 }, trailing: Button("Add") {
32 let newScrum = DailyScrum(
33 title: newScrumData.title,
34 attendees: newScrumData.attendees,
35 lengthInMinutes: Int(newScrumData.lengthInMinutes),
36 color: newScrumData.color)
37 $scrums.append(newScrum)
38 isPresented = false
39 })
40 }
41 }
42 }
43}

The DailyScrum objects are automatically loaded from the default Realm using the @ObservedResults annotation.

New scrums can be added to Realm by appending them to the scrums result set with $scrums.append(newScrum). Note that there's no need to open a Realm transaction explicitly. That's now handled under the covers by the Realm SDK.

#DetailView

The main change to DetailView is that any edits to a scrum are persisted immediately. At the time of writing (Realm-Cocoa 10.7.2), the view must open a transaction to store the change:

1do {
2 try Realm().write() {
3 guard let thawedScrum = scrum.thaw() else {
4 print("Unable to thaw scrum")
5 return
6 }
7 thawedScrum.update(from: data)
8 }
9} catch {
10 print("Failed to save scrum: \(error.localizedDescription)")
11}

#MeetingView

As with DetailView, MeetingView is enhanced so that meeting notes are added as soon as they've been created (rather than being stored in volatile RAM until the app is minimized):

1do {
2 try Realm().write() {
3 guard let thawedScrum = scrum.thaw() else {
4 print("Unable to thaw scrum")
5 return
6 }
7 thawedScrum.historyList.insert(newHistory, at: 0)
8 }
9} catch {
10 print("Failed to add meeting to scrum: \(error.localizedDescription)")
11}

#CardView (+ Other Views)

There are no changes needed to the view that's responsible for displaying a summary for a scrum. The changes we made to the DailyScrum model in order to store it in Realm don't impact how it's used within the app.

Scrum card view displaying the title, number of attendees and scrum duration
CardView

Similarly, there are no significant changes needed to EditView, HistoryView, MeetingTimerView, MeetingHeaderView, or MeetingFooterView.

#Summary

I hope that this post has shown that moving an iOS app to Realm is a straightforward process. The Realm SDK abstracts away the complexity of serialization and persisting data to disk. This is especially true when developing with SwiftUI.

Now that Scrumdinger uses Realm, very little extra work is needed to add new features based on filtering, synchronizing, and sharing data. Let me know in the community forum if you try adding any of that functionality.

#Resources

If you have questions, please head to our developer community website where the MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB.

MongoDB Icon
  • Developer Hub
  • Documentation
  • University
  • Community Forums

© MongoDB, Inc.