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.realmis 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)
}
}