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