realm-swift
realm-swift copied to clipboard
SwiftUI previews: Use in-memory database by default
When using SwiftUI's previews (PreviewProvider
) in Xcode and database access is triggered for any reason, Realm uses either the default configuration or the app's actual configuration. Both are problematic.
Goals
Create/modify Realm objects in SwiftUI PreviewProvider
in a transient way.
Expected Results
No database file is created/modified when using a PreviewProvider
and no crashes occur. Realm transactions in SwiftUI previews run completely separate from the app's database.
Actual Results
With default configuration:
-
~/Library/Containers/AppName/Data/Library/Application Support/default.realm
is being created/opened. - Possible crash of the preview if a Realm migration is required.
With app's actual sync configuration:
- Possible crash of the preview if a "real" build of the app is already running because "Multiple sync agents attempted to join the same session".
- Possible crash of the preview if a Realm migration is required.
Code Sample
import SwiftUI
import RealmSwift
@main
struct RealmTestApp: SwiftUI.App {
var body: some Scene {
WindowGroup {
CarList()
}
}
}
@objcMembers class Car: Object, ObjectKeyIdentifiable {
dynamic var model = ""
}
struct CarList: View {
@ObservedResults(Car.self) var cars
var body: some View {
Button("Add Car") {
let realm = try! Realm()
try! realm.write {
realm.add(Car())
}
}
List {
ForEach(cars, id: \.self) { car in
Text(String(car.id))
}
}
}
}
struct CarList_Previews: PreviewProvider {
static var previews: some View {
CarList()
}
}
Workaround
Of course, it's generally preferable to avoid creating any instances of RealmSwift.Object
for a PreviewProvider
, but that's not always feasible.
Unfortunately, there doesn't seem to be an easy way to globally set up a custom RealmSwift.Configuration
for Xcode's preview mode when using the SwiftUI lifecycle. Therefore, the configuration has to be set when the preview is being created (preferrably only once; the following is just for demonstrative purposes):
struct CarList_Previews: PreviewProvider {
static var previews: some View {
Realm.Configuration.defaultConfiguration = Realm.Configuration(
inMemoryIdentifier: "XcodePreview",
schemaVersion: 0
)
return CarList()
}
}
Environment
Xcode 12.5 Realm 10.7.4 (via SPM)
This sounds like a good idea to me if there's a good way to do it. I suspect there's some complications, but we should try to see what we can do to work better with Previews.
The following works, but I don't know nearly enough about Realm to guess what side-effects that might introduce: https://github.com/andreasley/realm-cocoa/commit/75211315a722b6c886a9705cec80501ffc1fae67
Maybe it would even be advisable to outright disable all non-transient actions when in Xcode preview mode. I don't see a use-case for Sync in previews (but lots of potential problems).
I'm using the following workaround. This creates two configurations - one is the 'main' and the other is 'preview'.
The preview is set to be 'in_memory'. I also load in sample data from a Data asset preview_realm.json in the Preview Assets.xcassets bundle.
I inject the configuration at App startup as below .environment(.realmConfiguration, RealmManager.main)
Here's the actual structure
struct RealmManager {
static var instance = RealmManager()
lazy var realm = try! Realm(configuration: RealmManager.main)
static let main: Realm.Configuration = {
var cfg = Realm.Configuration()
print("Created 'main' Realm Configuration")
return cfg
}()
static let preview: Realm.Configuration = {
var cfg = Realm.Configuration(inMemoryIdentifier: "in_memory")
do {
var realm = try Realm(configuration: cfg)
let schemaVersion = try Repository.fromBundledAsset("preview_realm", realm: realm)
print("In-memory schema: \(schemaVersion) for loaded preview realm")
} catch {
fatalError("Failed to load 'in_memory' Realm: \(error)")
}
return cfg
}()
}
For each preview I inject the configuration as below:-
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(
.environment(\.realmConfiguration, RealmManager.preview)
}
}
Seems to work just fine.
A year later... is there an official way of handling this? I could not find any information in the documentation on how to use Previews with Realm and this is essential to be able to have a good development experience with SwiftUI
I found working examples in the official documentation https://www.mongodb.com/docs/realm/sdk/swift/swiftui/swiftui-previews/
Whatever data you want to show in the preview needs to be added to the temporary realm. in my case Item data.
import Foundation
import RealmSwift
class MockRealms {
static var config: Realm.Configuration {
MockRealms.previewRealm.configuration
}
static var previewRealm: Realm = {
var realm: Realm
let identifier = "previewRealm"
let config = Realm.Configuration(inMemoryIdentifier: identifier)
do {
realm = try Realm(configuration: config)
try realm.write {
for index in 0...5 {
let item = Item()
item.summary = "toto \(index)"
realm.add(item)
}
}
return realm
} catch let error {
fatalError("Error: \(error.localizedDescription)")
}
}()
}
and then add configuration in the preview
import SwiftUI
import RealmSwift
struct ItemList: View {
@ObservedResults(Item.self) var items
var body: some View {
VStack {
List {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
.navigationBarTitle("Items", displayMode: .inline)
}
}
struct ItemList_Previews: PreviewProvider {
static var previews: some View {
ItemList()
.environment(\.realmConfiguration, MockRealms.config)
}
}