We compared the three most popular databases for iOS applications a while back. One of them was Realm, an alternative to Apple's Core Data. Realm Database is faster than both Core Data and SQLite, and easier to work with, too. It's free of charge for unlimited use unless you wish to take advantage of Realm's cloud features. In this tutorial, we're going to start a new iOS project with Realm to store application data. We'll cover basic CRUD (Create, read, update & delete,) operations with the database. You can find samples code here on Github.
Starting a New Project With Realm
Create an iOS Project
First, create a new iOS project with Xcode. Open the IDE and select File | New | Project.
We'll use a single view application with Swift. Make sure Use Core Data is not selected. We don't need it with Realm.
Then, select a folder to save the new project in.
Finally, after Xcode has created the project, it will open the IDE window.
Adding Realm Database
We'll use CocoaPods to download Realm and add it to our project. If you don't have CocoaPods installed yet, you can find installation instructions here. If you already have CocoaPods installed, update your repo.
$ pod repo update Updating spec repo `master` $ /usr/bin/git -C /Users/ericgoebelbecker/.cocoapods/repos/master fetch origin --progress remote: Enumerating objects: 8985, done. remote: Counting objects: 100% (8985/8985), done. remote: Compressing objects: 100% (60/60), done. remote: Total 23711 (delta 8941), reused 8922 (delta 8922), pack-reused 14726 Receiving objects: 100% (23711/23711), 2.58 MiB | 12.44 MiB/s, done. Resolving deltas: 100% (16160/16160), completed with 2886 local objects. From https://github.com/CocoaPods/Specs 23fe1ee72df..852d0200946 master -> origin/master $ /usr/bin/git -C /Users/ericgoebelbecker/.cocoapods/repos/master rev-parse --abbrev-ref HEAD master $ /usr/bin/git -C /Users/ericgoebelbecker/.cocoapods/repos/master reset --hard origin/master Checking out files: 100% (2546/2546), done. HEAD is now at 852d0200946 [Add] LocalizationKit 4.2.1
Depending on how long it's been since you updated, your output may look different. Next, create a new file named podfile in the root directory of the new iOS project. A pod file is a plain text file so that you can use your favorite text editor. You need to specify the RealmSwift framework and add an additional directive both the main project and the test project. Here are the contents of the podfile:
platform :ios, 12.0 target 'RealmTutorial' do use_frameworks! pod 'RealmSwift' target 'RealmTutorialTests' do use_frameworks! end end
If you didn't name your project "RealmTutorial," change the pod file contents to reflect the project name. Now, run pod install to install the library.
$ pod install Analyzing dependencies Downloading dependencies Installing Realm (3.11.0) Generating Pods project Integrating client project [!] Please close any current Xcode sessions and use `SampleRealmApp.xcworkspace` for this project from now on. Sending stats Pod installation complete! There are two dependencies from the Podfile and one total pod installed.
This may take a few minutes to run since the Realm framework is large. After pod finishes the install, close Xcode and reopen the project with RealmTutorial.xcworkspace and use this file from now on, as pod instructed on the command line. We're ready to write some code.
Using the Realm Database to Persist Application Data
Create an Object
First, we'll create a class that we can store in the Realm database. Add a swift file named ComicBook.swift to the project, with the following code.
import RealmSwift class ComicBook: Object { @objc dynamic var title = "" @objc dynamic var character = "" @objc dynamic var issue = 0 }
We import RealmSwift into the class file and then extend RealmSwift's Object. That's all we need to do to prepare a class for use in Realm.
Create and Use a Database
Next, we need to write the code to save and retrieve ComicBooks from Realm. Let's create a class called ComicBookStore, because I like puns. Put this code in a file name ComicBookStore.swift
import Foundation import RealmSwift class ComicBookStore { var realm: Realm = try! Realm() }
We're creating the Realm database as a class member. By calling the default constructor, we're using the default instance. We'll take a closer look at what that means below. The constructor can throw, so we need a try! with the call. Let's add the methods we need for the first test one at a time. First, saving an object.
public func saveComicBook(_ comic: ComicBook) { try! realm.write { realm.add(comic) } }
Realm.write starts a write transaction. A write transaction can fail, so it's marked as throwing. It's possible to recover from some errors, such as running out of space, but we'll skip that in this tutorial. It's something you should consider in production code, though. Since ComicBook is an Object, we can add it to Realm. Add can throw, so we mark it with a try. Next, retrieving an object.
public func findComicsByTitle(_ title: String) -> Results<ComicBook> { let predicate = NSPredicate(format: "title = %@", title) return realm.objects(ComicBook.self).filter(predicate) }
This will find all ComicBook instances in the database that having a matching title field. NSPredicate is a convenient way to build a query string. Realm supports a large set of search operators, all of which can be built with NSPredicate. Realm has a handy cheatsheet. The syntax for finding a title matching "The Incredible Hulk" looks like this.
"title = 'The Incredible Hulk'"
The search returns a ResultSet. We'll see how to use it below. Finally, we need a way to create ComicBooks. Let's add this method to ComicBookStore, even though in a production application there would be a better place for it.
public func makeNewComicBook(_ title: String, character: String, issue: Int ) -> ComicBook { let newComic = ComicBook() newComic.title = title newComic.character = character newComic.issue = issue return newComic }
Testing the Database
We have enough code to run a simple test. We'll use unit tests for this tutorial to keep it short and focused.
func testSaveAndGetComic() { let comic = comicStore.makeNewComicBook( "Amazing Spider-Man", character: "Punisher", issue: 129) comicStore.saveComicBook(comic) let foundComics = comicStore.findComicsByTitle("Amazing Spider-Man") XCTAssertEqual(foundComics.count, 1) let comic1 = foundComics.first XCTAssertEqual(comic1?.issue, 129) }
We create a comic, save it, and then look it up using the title. It's a busy test method, but it illustrates what we're trying to do. Then, we check the count field in the ResultSet. We expect to find one. Finally, we verify the issue number is 129. Run the test. It passes. Rerun the test. It fails! On the second run, Realm returns two ComicBooks from the search request. Why? When we create our database with Realm(), we're using the default instance. Realm opens a default database file in the Documents folder for an iOS application or Application Support folder for a macOs application. If the file doesn't already exist, Realm creates it. When we ran the first test, Realm created the file. We added a ComicBook to the database. When we ran the second test, Realm reopened the file, and we added a second comic. There are two objects when we search, and the test fails.
Fixing the Test With Dependency Injection
So, what did we learn? That Realm's databases are reused between application (and test) runs, and that Realm supports duplicate records. Let's fix our tests and also learn how to override Realm's default configuration. First, modify ComicBookStore to accept a Realm instance instead of creating one for itself. There are several ways to implement this pattern. Let's go with the simplest.
enum RuntimeError: Error { case NoRealmSet } class ComicBookStore { var realm: Realm? public func saveComicBook(_ comic: ComicBook) throws { if (realm != nil) { try! realm!.write { realm!.add(comic) } } else { throw RuntimeError.NoRealmSet } } public func findComicsByTitle(_ title: String) throws -> Results<ComicBook> { if (realm != nil) { let predicate = NSPredicate(format: "title = %@", title) return realm!.objects(ComicBook.self).filter(predicate) } else { throw RuntimeError.NoRealmSet } } }
So, we need to set ComicBookStore's realm field before we use it, or it will throw an error. Next, modify the test class. First, add a single line to the test setup.
override func setUp() { Realm.Configuration.defaultConfiguration.inMemoryIdentifier = self.name let realm = try! Realm() comicStore.realm = realm }
We're configuring Realm to use an in-memory database, and setting the identifier to the test instance's name. Then we create the database and pass it to the ComicBookStore. Next, add a few lines to our test method.
func testSaveAndGetComic() { do { let comic = comicStore.makeNewComicBook( "Amazing Spider-Man", character: "Punisher", issue: 129) try comicStore.saveComicBook(comic) let foundComics = try comicStore.findComicsByTitle("Amazing Spider-Man") XCTAssertEqual(foundComics.count, 1) let comic1 = foundComics.first XCTAssertEqual(comic1?.issue, 129) } catch RuntimeError.NoRealmSet { XCTAssert(false, "No realm database was set") } catch { XCTAssert(false, "Unexpected error \(error)") } }
We need to add tries and catches to the tests since we had to mark the save and find methods as throwing. So, we have the building blocks for adding new comics to the database and retrieving them by title. Adding a search by character and issue number would be simple. Let's wrap up with updating and deleting books.
Update Objects
The easiest way to update an object in Realm is to modify an object that you already save to the database once. Modify it inside a write transaction.
try! realm.write { comic.character = "Aquaman" }
This is a very powerful feature, but it requires access to the database in the same place as the objects and an object that is already bound to the database. We can use key-value coding to update items in a ResultSet, and the changes are automatically saved. The process consists of two steps:
Find the objects we want to update
Call the ResultSet with a predicate describing how to update them
Here's the code:
public func updateComicBooks(_ field: String, currentValue: String, updatedValue: String) throws { let comics = try findComicsByField(field, value: currentValue) try! realm!.write { comics.setValue(updatedValue, forKeyPath: "\(field)") } } private func findComicsByField(_ field: String, value: String) throws -> Results<ComicBook> { if (realm != nil) { let predicate = NSPredicate(format: "%K = %@", field, value) return realm!.objects(ComicBook.self).filter(predicate) } else { throw RuntimeError.NoRealmSet } }
First, we call the new findComicsByField method, which accepts both the field to search for and the desired value. It uses NSPredicate to build a search expression. We can refactor our findComicsByTitle method to use this too. This returns a ResultSet containing the comics we want to modify. Finally, we enter a write block and use update to update the records. We don't need to check if we have a Realm instance since the find call would have already thrown. Let's write a test.
func testSaveAndUpdateComic() { do { let newComic = comicStore.makeNewComicBook( "The Incredible Hulk", character: "Wendigo", issue: 181) try comicStore.saveComicBook(newComic) let foundComics = try comicStore.findComicsByTitle("The Incredible Hulk") let foundComic = foundComics.first XCTAssertEqual(foundComic?.character, "Wendigo") try comicStore.updateComicBooks("character", currentValue: "Wendigo", updatedValue: "Wolverine") let changedComics = try comicStore.findComicsByTitle("The Incredible Hulk") XCTAssertEqual(changedComics.count, 1) let changedComic = changedComics.first XCTAssertEqual(changedComic?.character, "Wolverine") } catch RuntimeError.NoRealmSet { XCTAssert(false, "No realm database was set") } catch { XCTAssert(false, "Unexpected error \(error)") } }
First, we create a copy of Incredible Hulk #181, which guest-starred Wendigo, and we save it. Then, we remembered that Hulk #181 was Wolverine's first appearance too! We need to update the record now! So we called updateComicBooks and altered the record. Then we checked and verified that Realm made the change. Before we move on to deleting records, let's refactor findComicsByTitle to take advantage of our new private method and rerun all of our tests.
public func findComicsByTitle(_ title: String) throws -> Results<ComicBook> { return try findComicsByField("title", value: title) }
That works! We could easily add methods for character and issue number, too.
Delete Objects
Let's start with a naive delete method.
public func deleteComicBook(_ comicBook: ComicBook) throws { if (realm != nil) { try! realm!.write { realm!.delete(comicBook) } } else { throw RuntimeError.NoRealmSet } }
And then a test.
func testDelete() { do { let newComic = comicStore.makeNewComicBook( "Captain America", character: "Red Skull", issue: 183) try comicStore.saveComicBook(newComic) let newerComic = comicStore.makeNewComicBook( "Captain Ameria", character: "Red Skull", issue: 183) try comicStore.deleteComicBook(newerComic) let foundComics = try comicStore.findComicsByTitle("Captain America") XCTAssertEqual(foundComics.count, 0) } catch RuntimeError.NoRealmSet { XCTAssert(false, "No realm database was set") } catch { XCTAssert(false, "Unexpected error \(error)") } }
This test generates an exception. "RLMException", "Can only delete an object from the Realm it belongs to." We tried to delete a ComicBook that wasn't saved in the database. If we eliminate creating newerComic and delete newComic, the test passes. But what if we want to delete items based on search terms entered by a user? Let's write a more robust delete that takes advantage of a more advanced search.
public func deleteComicBook(_ comicBook: ComicBook) throws { if (realm != nil) { let predicate = NSPredicate(format: "title == %@ AND character == %@ AND issue == %d", comicBook.title, comicBook.character, comicBook.issue) let targetComics = realm!.objects(ComicBook.self).filter(predicate) var comics = targetComics.makeIterator() while let comic = comics.next() { try! realm!.write { realm!.delete(comic) } } } else { throw RuntimeError.NoRealmSet } }
So, rather than assuming we've been passed a ComicBook that is in the default realm, we use its three fields to find one (or more) that is. This predicate demonstrates how Realm supports boolean logic. The search may return more than one march, since our default realm allows duplicates, so we iterate through the list and delete them all. Rerun the test. It passes!
You Take It From Here
We've created a new project, added support for Realm, and then implemented the core CRUD operations: create, read, update, and delete. You can start from here and build a robust iOS application with Realm database. Real has some very powerful cloud features too, if you want to take your application to the next level. While you're at it, you can use CloudBees Feature Management's platform to manage new deploying features and run user tests. Sign up for a free demo here.