realm-swift icon indicating copy to clipboard operation
realm-swift copied to clipboard

SwiftUI previews: Use in-memory database by default

Open andreasley opened this issue 3 years ago • 4 comments

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)

andreasley avatar Apr 25 '21 20:04 andreasley

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.

tgoyne avatar Apr 27 '21 14:04 tgoyne

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

andreasley avatar May 01 '21 14:05 andreasley

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.

alan16R avatar Dec 29 '21 17:12 alan16R

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

sipersso avatar May 28 '22 16:05 sipersso

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

gahntpo avatar Mar 05 '23 10:03 gahntpo