After three years of work, we're proud to announce the public release of Realm Cocoa 5.0, with a ground-up rearchitecting of the core database.
In the time since we first released the Realm Mobile Database to the world in 2014, we've done our best to adapt to how people have wanted to use Realm and help our users build better apps, faster. Some of the difficulties developers ran into came down to some consequences of design decisions we made very early on, so in 2017 we began a project to rethink our core architecture. In the process, we came up with a new design that simplified our code base, improves performance, and lets us be more flexible around multi-threaded usage.
In case you missed a similar writeup for Realm Java with code examples you can find it here.
#Frozen Objects
One of the big new features this enables is Frozen Objects.
One of the core ideas of Realm is our concept of live, thread-confined objects that reduce the code mobile developers need to write. Objects are the data, so when the local database is updated for a particular thread, all objects are automatically updated too. This design ensures you have a consistent view of your data and makes it extremely easy to hook the local database up to the UI. But it came at a cost for developers using reactive frameworks.
Sometimes Live Objects don't work well with Functional Reactive Programming (FRP) where you typically want a stream of immutable objects. This means that Realm objects have to be confined to a single thread. Frozen Objects solve both of these problems by letting you obtain an immutable snapshot of an object or collection which is fully thread-safe, without copying it out of the realm. This is especially important with Apple's release of Combine and SwiftUI, which are built around many of the ideas of Reactive programming.
For example, suppose we have a nice simple list of Dogs in SwiftUI:
1 class Dog: Object, ObjectKeyIdentifable { 2 dynamic var name: String = "" 3 dynamic var age: Int = 0 4 } 5 6 struct DogList: View { 7 @ObservedObject var dogs: RealmSwift.List<Dog> 8 9 var body: some View { 10 List { 11 ForEach(dogs) { dog in 12 Text(dog.name) 13 } 14 } 15 } 16 }
If you've ever tried to use Realm with
SwiftUI, you can probably see a
problem here: SwiftUI holds onto references to the objects passed to
ForEach()
, and if you delete an object from the list of dogs it'll crash
with an index out of range error. Solving this used to involve
complicated workarounds, but with
Realm Cocoa 5.0 is as simple as
freezing the list passed to ForEach()
:
1 struct DogList: View { 2 @ObservedObject var dogs: RealmSwift.List<Dog> 3 4 var body: some View { 5 List { 6 ForEach(dogs.freeze()) { dog in 7 Text(dog.name) 8 } 9 } 10 } 11 }
Now let's suppose we want to make this a little more complicated, and group the dogs by their age. In addition, we want to do the grouping on a background thread to minimize the amount of work done on the main thread. Fortunately, Realm Cocoa 5.0 makes this easy:
1 struct DogGroup { 2 let label: String 3 let dogs: [Dog] 4 } 5 6 final class DogSource: ObservableObject { 7 @Published var groups: [DogGroup] = [] 8 9 private var cancellable: AnyCancellable? 10 init() { 11 cancellable = try! Realm().objects(Dog.self) 12 .publisher 13 .subscribe(on: DispatchQueue(label: "background queue")) 14 .freeze() 15 .map { dogs in 16 Dictionary(grouping: dogs, by: { $0.age }).map { DogGroup(label: "\($0)", dogs: $1) } 17 } 18 .receive(on: DispatchQueue.main) 19 .assertNoFailure() 20 .assign(to: \.groups, on: self) 21 } 22 deinit { 23 cancellable?.cancel() 24 } 25 } 26 27 struct DogList: View { 28 @EnvironmentObject var dogs: DogSource 29 30 var body: some View { 31 List { 32 ForEach(dogs.groups, id: \.label) { group in 33 Section(header: Text(group.label)) { 34 ForEach(group.dogs) { dog in 35 Text(dog.name) 36 } 37 } 38 } 39 } 40 } 41 }
Because frozen objects aren't thread-confined, we can subscribe to change notifications on a background thread, transform the data to a different form, and then pass it back to the main thread without any issues.
#Combine Support
You may also have noticed the .publisher
in the code sample above.
Realm Cocoa 5.0 comes with
basic built-in support for using Realm objects and
collections with Combine.
Collections (List, Results, LinkingObjects,
and AnyRealmCollection) come with a .publisher
property which emits the
collection each time it changes, along with a .changesetPublisher
property that emits a RealmCollectionChange<T>
each time the collection
changes. For Realm objects, there are similar publisher()
and
changesetPublisher()
free functions which produce the equivalent for
objects.
For people who want to use live objects with
Combine, we've added a
.threadSafeReference()
extension to Publisher
which will let you safely
use receive(on:)
with thread-confined types. This lets you write things
like the following code block to easily pass thread-confined objects or collections between threads.
1 publisher(object) 2 .subscribe(on: backgroundQueue) 3 .map(myTransform) 4 .threadSafeReference() 5 .receive(on: .main) 6 .sink {print("\($0)")}
#Queue-confined Realms
Another threading improvement coming in Realm Cocoa 5.0 is the ability to confine a realm to a serial dispatch queue rather than a thread. A common pattern in Swift is to use a dispatch queue as a lock which guards access to a variable. Historically, this has been difficult with Realm, where queues can run on any thread.
For example, suppose you're using URLSession and want to access a Realm each time you get a progress update. In previous versions of Realm you would have to open the realm each time the callback is invoked as it won't happen on the same thread each time. With Realm Cocoa 5.0 you can open a realm which is confined to that queue and can be reused:
1 class ProgressTrackingDelegate: NSObject, URLSessionDownloadDelegate { 2 public let queue = DispatchQueue(label: "background queue") 3 private var realm: Realm! 4 5 override init() { 6 super.init() 7 queue.sync { realm = try! Realm(queue: queue) } 8 } 9 10 public var operationQueue: OperationQueue { 11 let operationQueue = OperationQueue() 12 operationQueue.underlyingQueue = queue 13 return operationQueue 14 } 15 16 func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) { 17 guard let url = downloadTask.originalRequest?.url?.absoluteString else { return } 18 try! realm.write { 19 let progress = realm.object(ofType: DownloadProgress.self, forPrimaryKey: url) 20 if let progress = progress { 21 progress.bytesWritten = totalBytesWritten 22 } else { 23 realm.create(DownloadProgress.self, value: [ 24 "url": url, 25 "bytesWritten": bytesWritten 26 ]) 27 } 28 } 29 } 30 } 31 let delegate = ProgressTrackingDelegate() 32 let session = URLSession(configuration: URLSessionConfiguration.default, 33 delegate: delegate, 34 delegateQueue: delegate.operationQueue)
You can also have notifications delivered to a dispatch queue rather
than the current thread, including queues other than the active one.
This is done by passing the queue to the observe function:
let token = object.observe(on: myQueue) { ... }
.
#Performance
With Realm Cocoa 5.0, we've greatly improved performance in a few important areas. Sorting Results is roughly twice as fast, and deleting objects from a Realm is as much as twenty times faster than in 4.x. Object insertions are 10-25% faster, with bigger gains being seen for types with primary keys.
Most other operations should be similar in speed to previous versions.
Realm Cocoa 5.0 should also typically produce smaller Realm files than previous versions. We've adjusted how we store large binary blobs so that they no longer result in files with a large amount of empty space, and we've reduced the size of the transaction log that's written to the file.
#Compatibility
Realm Cocoa 5.0 comes with a new version of the Realm file format. Any existing files that you open will be automatically upgraded to the new format, with the exception of read-only files (such as those bundled with your app). Those will need to be manually upgraded, which can be done by opening them in Realm Studio or recreating them through whatever means you originally created the file. The upgrade process is one-way, and realms cannot be converted back to the old file format.
Only minor API changes have been made, and we expect most applications
which did not use any deprecated functions will compile and work with no
changes. You may notice some changes to undocumented behavior, such as
that deleting objects no longer changes the order of objects in an
unsorted Results
.
Pre-1.0 Realms containing Date
or Any
properties can no longer be
opened.
Want to try it out for yourself? Check out our working demo app using Frozen Objects, SwiftUI, and Combine.
- Simply clone the realm-cocoa repo and open
RealmExamples.xworkspace
then select theListSwiftUI
app in Xcode and Build.
#Wrap Up
We're very excited to finally get these features out to you and to see what new things you'll be able to build with them. Stay tuned for more exciting new features to come; the investment in the Realm Database continues.
#Links
Want to learn more? Review the documentation..
Ready to get started? Get Realm Core 6.0 and the SDK's.
Want to ask a question? Head over to our MongoDB Realm Developer Community Forums.