If you prefer videos over written articles, this article is a written version of a video I produced. The content is identical.
This video is the first in a series that I hope will serve to teach you how to use iOS frameworks and tools such as Siri Shortcuts, CloudKit, and more. If you have a specific framework or feature you'd like to see this series cover, feel free to write me at jordan[dot]osterberg[at]shadowsystems[dot]tech, or on Twitter @josterbe1.
Table of Contents
Without further ado, let's take a look at the application we're going to build in this series...
Introduction
We have our list of notes, and when we tap into one, we can read and begin to edit, or delete it. Pretty simple. You'll notice if you download the starter project that notes don't persist when you go in and out of the NoteDetailController when tapping on the "Apple Park Visit" note. That is, the content of the note doesn't save when you edit it.
That's what we're going to build out in this article, using the Realm Database. I've used Realm for just about two years, and I find its simplicity to outweigh the cost of using a 3rd party library. In fact, I use it in the app I spend most of my personal development time on (see https://countdowns.download) on both macOS and iOS.
Installing Dependencies
In order to begin using Realm, we need to install it using CocoaPods. CocoaPods, if you're not aware, CocoaPods is a dependency management tool that is widely used in the iOS space. If you don't have CocoaPods installed on your Mac already, you can use the sudo gem install cocoapods
command to get started. Now, open the project folder, as well as a Terminal window inside of that directory.
Type in pod init
, and then open Podfile
.
Inside of the newly created "Podfile", we need to write some text which will inform CocoaPods what libraries you'd like to install into your project.
Below # Pods for NotesApp
, write these two lines:
pod 'Realm', '~> 3.12.0'
pod 'RealmSwift', '~> 3.12.0'
Your Podfile should look similar to this after you've written those lines:
platform :ios, '12.0'
target 'NotesApp' do
use_frameworks!
# Pods for NotesApp
pod 'Realm', '~> 3.12.0'
pod 'RealmSwift', '~> 3.12.0'
target 'NotesAppTests' do
inherit! :search_paths
end
target 'NotesAppUITests' do
inherit! :search_paths
end
end
Now that we've added our dependencies, let's ask CocoaPods to install them with pod install
This will take some time when you first use CocoaPods. Don't worry, CocoaPods is just downloading some initial components. This won't happen every time you pod install
.
Your Terminal window will look like this once that command finishes executing:
After this, if you've opened the NotesApp.xcodeproj
already, close out of it. When using CocoaPods, you must use the .xcworkspace
file instead of the default .xcodeproj
file. Open the NotesApp.xcworkspace
file and head to Note.swift
.
The Model
This class contains our Note model object, which contains a few basic properties:
class Note {
var identifier: String
var content: String
var lastEdited: Date
init(
identifier: String = UUID().uuidString,
content: String,
lastEdited: Date = Date()) {
self.identifier = identifier
self.content = content
self.lastEdited = lastEdited
}
}
Standard model code, nothing special going on here.
We also have an extension to our Note object in the same file, which subclasses as protocol called Writeable
extension Note: Writable {
func write(dataSource: DataSource) {
self.lastEdited = Date()
dataSource.store(object: self)
}
func delete(dataSource: DataSource) {
dataSource.delete(object: self)
}
}
Inside of the write
and delete
functions, you'll notice we have a DataSource
property. DataSource
is a generic protocol to help make modifying data as abstract at the higher levels of our code as possible.
Here's the protocol definition:
protocol DataSource {
func store<T>(object: T)
func delete<T>(object: T)
}
If you're not familiar with generics, each of our functions has a T
parameter which essentially means it can be any type of object. This isn't used very heavily in our project, but it could be evolved and used further to create multiple DataSources with different constraints around what objects they can store.
We implement our DataSource
protocol in NoteDataSource
. There isn't anything special here either, aside from one little tidbit I'd like to note for explanation's sake.
Whenever we store
or delete
objects, we use the following call to NotificationCenter
:
NotificationCenter.default.post(name: .noteDataChanged, object: nil)
// We also have this extension of Notification.Name to make sending and receiving this notification simple.
extension Notification.Name {
static let noteDataChanged = Notification.Name(rawValue: "noteDataChanged")
}
Essentially, whenever any note is stored or deleted, we inform any listeners that our data has changed, so they can update their UI accordingly.
With all of our model files and classes out of the way, let's begin implementing Realm!
Implementing Realm
There are three steps to integrate Realm with our project:
- Creating the Realm Object
- Bridging between our Realm object and our primitive Note object
- Begin retrieving and modifying data with Realm
Creating the Realm Object
This step is relatively simple. Let's create a new Swift file called RealmNote.swift
. Inside of RealmNote.swift
, import the RealmSwift framework and create a class declaration like so:
import RealmSwift
class RealmNote: Object {
}
We'll subclass Realm's Object
class which will allow us to use RealmNote
in Realm database functions.
Now, add in the three properties that we have in our Note model:
@objc dynamic var identifier: String = ""
@objc dynamic var content: String = ""
@objc dynamic var lastEdited: Date = Date()
The @objc dynamic
pieces of our variable declaration expose our properties to Objective-C, which many of the iOS layers of Realm are written in. It also allows Realm to listen for changes to our RealmNote
objects.
To finish off our class, override the class func
primaryKey, which returns an optional string (String?
) with the value "identifier". This informs Realm that RealmNote
's primary key, a way to uniquely identify our objects, is stored in the "identifier" property.
Once you've completed these steps, your RealmNote
will look like this:
class RealmNote: Object {
@objc dynamic var identifier: String = ""
@objc dynamic var content: String = ""
@objc dynamic var lastEdited: Date = Date()
override class func primaryKey() -> String? {
return "identifier"
}
}
That's all for step one.
Bridging between our Realm object and our primitive Note object
We have two separate model objects: Note
and RealmNote
. RealmNote is used internally when dealing with Realm, in order to keep our model layer decoupled from our UI. By using two separate objects, we could switch away from using Realm in the future if the need arrises.
In the RealmNote.swift
file, create an extension of RealmNote
:
extension RealmNote {
}
Now, create a convenience init
inside of the extension which takes in a Note
as it's only property. This will allow us to create RealmNote
objects by using a Note
object.
convenience init(note: Note) {
self.init()
self.identifier = note.identifier
self.content = note.content
self.lastEdited = note.lastEdited
}
Great, now, to finish RealmNote.swift
off, create a Note
variable inside the extension which initializes from a RealmNote
:
var note: Note {
return Note(realmNote: self)
}
Don't worry if Xcode gives you an error about Note's initializer, we're about to fix that.
Head over to Note.swift
, where we're about to write the other half of our bridge. This code is essentially the same thing as RealmNote
's extension.
extension Note {
convenience init(realmNote: RealmNote) {
self.init(identifier: realmNote.identifier, content: realmNote.content, lastEdited: realmNote.lastEdited)
}
var realmNote: RealmNote {
return RealmNote(note: self)
}
}
Great, that's step two. We can now access a RealmNote
from a Note
, and a Note
from a RealmNote
. This also means that this code is perfectly valid:
Note(content: "Example").realmNote.note.realmNote.note.realmNote.note.realmNote
// and so on...
Jokes aside, let's finish our application with step three.
Begin retrieving and modifying data with Realm
Head into NoteDataSource.swift
, and import RealmSwift
before the class declaration. Before we modify, delete, and retrieve Realm objects, we need an instance of the Realm Database. Replace NoteDataSource
's init with this:
var realm: Realm
init() {
// Load our data
self.realm = try! Realm()
}
This will create an instance of the Realm database. In production, you'd likely not want to use the bang (!) operator when creating the instance because your application will crash if the database isn't available.
Next, let's edit the store
function to actually store objects into our database.
Writing with Realm is simple:
try? self.realm.write {
}
Inside of that code block, we can update objects within the database. Write this:
self.realm.add(note.realmNote, update: true)
This will either create a new RealmNote
in the database or update an existing one if one exists. That's it! We're now storing objects in the Realm database (calling note's write function is performed in NoteDetailController.swift
if you'd like to see the function that is actually used to perform this write.)
Now, let's write our delete
function. Because of the way we wrote our bridge (without consulting Realm whenever we create a RealmNote
from a Note
), we must fetch the object directly from the database using the note's identifier rather than using it's realmNote
property.
This is simple:
if let realmNote = self.realm.object(ofType: RealmNote.self, forPrimaryKey: note.identifier) {
}
This asks Realm to retrieve a RealmNote
object from the database, with the identifier, or primary key, of note.identifier
.
This can be a bit funky. For some reason, the application crashes if we use the traditional write
function on Realm. As a workaround, this code is perfectly valid and essentially performs the same task as write
:
self.realm.beginWrite()
self.realm.delete(realmNote)
try? self.realm.commitWrite()
We begin our write, we delete the object, and we commit our write.
With that, we've built a functioning note application!
Conclusion
I hope you've enjoyed this tutorial on how to use the Realm Database. I've had a lot of fun making it and I can't wait to evolve this series and add more features to our app over time such as Siri Shortcuts, CloudKit, and more. Thanks for reading.
Top comments (5)
What's the difference between the DataSource and NoteDataSource? I've seen other projects use a DataSource or a RealmManager in order to create generic functions but I was curious as to why there were separate protocols for the DataSource.
DataSource is the generic protocol for storing data, and NoteDataSource is the implementation for the Note data type.
Thanks, Jordan!
Sorry. I can't get it to "import RealmSwift"
Some comments may only be visible to logged-in visitors. Sign in to view all comments.