iosswiftswiftuicore-data

How to Bind .environment Globally to All SwiftUI Previews?


I have a global DataManager that holds an NSManagedObjectContext:

class DataManager {
    static let shared = DataManager()
    var viewContext: NSManagedObjectContext
    // ... Other code
}

Everything works fine within the application itself. However, I'm running into issues with SwiftUI previews. Currently, I'm setting the .environment for each preview like this:

#Preview {
    let exampleModel = ExampleModel.create(context: DataManager.shared.viewContext)
    
    return NavigationStack {
        ExampleView(model: exampleModel)
    }
    .environment(\.managedObjectContext, DataManager.shared.viewContext)
}

This approach becomes tedious when dealing with multiple views. I'm looking for a way to set .environment(\.managedObjectContext, DataManager.shared.viewContext) globally for all SwiftUI previews.

Is there a more efficient way to achieve this?

I've done some research and it seems feasible to create a wrapper for previews. However, I haven't found a solution that allows me to continue using the #Preview macro.


Solution

  • You can create a ViewModifier with all your injection and sample code.

    extension View {
        func globalInjection() -> some View {
            modifier(GlobalInjectionVM())
        }
    }
    struct GlobalInjectionVM: ViewModifier {
        let context: NSManagedObjectContext = DataManager.shared.viewContext
        func body(content: Content) -> some View {
            content
                .environment(\.managedObjectContext, context)
        }
    }
    

    Then all you need to do is call the func at every preview.

    #Preview {
        SampleView()
            .globalInjection()
    }
    

    If you want to step this up you can easily create preview objects with a protocol

    protocol SampleProviderProtocol {
        static func preview() -> Self
    }
    
    extension Item: SampleProviderProtocol {
        static func preview() -> Self {
            let new = Self(context: DataManager. shared.viewContext)
            new.timestamp = Calendar.current.date(byAdding: .day, value: (-10...10).randomElement()!, to: Date())
            return new
        }
    }
    

    Then you can dynamically create them in the ViewModifier

    struct GlobalInjectionVM: ViewModifier {
        let context: NSManagedObjectContext = DataManager.shared.viewContext
        //Declare the types that you want to create samples 
        let types: [SampleProviderProtocol.Type] = [Item.self]
        func body(content: Content) -> some View {
            content
                .environment(\.managedObjectContext, context)
                .task {
                    //Iterate over the types
                    for type in types {
                        //Create 4 preview objects
                        for _ in 0...3 {
                            _ = type.preview()
                        }
                    }
                }
        }
    }
    

    Make sure you are using a "Preview" context when working with Canvas you can to it by setting the url to null before loading the store.

    container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    

    PreviewModifier has been introduced in

    iOS 18.0+ iPadOS 18.0+ Mac Catalyst 18.0+ macOS 15.0+ tvOS 18.0+ visionOS 2.0+ watchOS 11.0+

    It is very similar to ViewModifier but has the advantage that you can use traits for injection.

    #Preview(traits: .coreDataNull) { // <--- Here
        GlobalPreviewInjectionSampleView()
    }
    

    Here is a sample of the new modifier.

    struct CoreDataPreview: PreviewModifier {
        let types: [SampleProviderProtocol.Type] = [Item.self]
    
        static func makeSharedContext() async throws -> NSManagedObjectContext {
            //Something that includes
            //container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
            return .init(concurrencyType: .mainQueueConcurrencyType)
        }
        
        typealias Context = NSManagedObjectContext
    
        
        func body(content: Content, context: Context) -> some View {
            content
                .environment(\.managedObjectContext, context)
                .task {
                    //Iterate over the types
                    for type in types {
                        //Create 4 preview objects
                        for _ in 0...3 {
                            _ = type.preview()
                        }
                    }
                }
        }
    }
    
    extension PreviewTrait where T == Preview.ViewTraits {
        @MainActor static var coreDataNull: Self = .modifier(CoreDataPreview())
    }