loading...

Realm Database Guide - Building a Note app in Swift for iOS

jordanosterberg profile image Jordan Osterberg Updated on ・7 min read

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

  1. Introduction
  2. Installing Dependencies
  3. The Model
  4. Implementing Realm
  5. Conclusion

Without further ado, let's take a look at the application we're going to build in this series...

Introduction

The application's list of notes
Editing a note individually

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:

Terminal window

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:

  1. Creating the Realm Object
  2. Bridging between our Realm object and our primitive Note object
  3. 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.

Discussion

pic
Editor guide
Collapse
lianahaque profile image
Liana Haque

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.

Collapse
jordanosterberg profile image
Jordan Osterberg Author

DataSource is the generic protocol for storing data, and NoteDataSource is the implementation for the Note data type.

Collapse
lianahaque profile image
Liana Haque

Thanks, Jordan!

Collapse
r_a_chalmers profile image
Robert A. Chalmers

Sorry. I can't get it to "import RealmSwift"

Collapse
hlc0000 profile image
Info Comment marked as low quality/non-constructive by the community. View code of conduct
hlc0000

Hello, I'm an IOS developer. Recently, when using swift to develop applications, I found that there are few caches written by pure swift. So I wrote a cache -- swiftlycache, a lightweight general-purpose IOS cache library using swift 5. If you are using swift for development, if you also need to use cache, maybe you can try swiftlycache, maybe you will like it, If you like, you can also introduce it to your friends. Thank you
github.com/hlc0000/SwiftlyCache