SwiftUI Previews with Core Data
SwiftUI previews are powerful tools that help us to see our views live. However, it is tricky to manage local storage-related views with previews, most of the time, you might need to create an entity instance to be able to run a preview. In this article, we will quickly go through how we can avoid entity object creation for SwiftUI Previews
.
Problem Statement
We have an initial project to state the problems, a shopping list application. Visit the initial repo from here, and quickly review the project. Please open ShoppingListItemsView
and scroll to the bottom, you will see this code block:
struct ShoppingListItemsView_Previews: PreviewProvider {
static var previews: some View {
let shoppingList = ShoppingList(context: PersistenceManager.shared.container.viewContext)
let item1 = ShoppingListItem(context: PersistenceManager.shared.container.viewContext)
let item2 = ShoppingListItem(context: PersistenceManager.shared.container.viewContext)
item1.name = "Item 1"
item2.name = "Item 2"
item1.amount = 1
item2.amount = 2
shoppingList.items = [item1, item2]
return ShoppingListItemsView(shoppingList: shoppingList)
.environmentObject(PersistenceManager.shared)
}
}
As you can see, some entity objects are created here to be able to show them on the preview. But, this is a problem, try to run the preview and add some items to the list. You will be facing some crashes soon enough because the dummy items are not saved to the disk and there will be confusion in the preview. Also, we are accessing the shared instance of PersistenceManager
and it is not needed unless you have some app extensions and need to use the storage within the extension! Besides that, we should be able to avoid singleton objects as much as possible due to testing purposes.
The last issue with this code is that we are using the actual local storage instance for the preview and it is not needed since it is just a preview for a specific view.
Using Core Data in Memory
First of all, we will be adding new support for our PersistenceManager
to be able to read-write support on memory rather than keeping on disk. Open PersistenceManager.swift
and update the initialization as follows:
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CoreDataPreviews")
// Use memory as a storage
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
...
}
Within this change, we will be able to use core data on memory for previews.
New Persistent Manager for Previews
Let's create a new singleton instance of persistence manager for preview:
extension PersistenceManager {
static var preview: PersistenceManager = {
let manager = PersistenceManager(inMemory: true)
let list1 = manager.saveList(with: "Preview list 1")
let list2 = manager.saveList(with: "Preview list 2")
manager.saveItem(name: "Preview item 1", amount: 1, for: list1)
manager.saveItem(name: "Preview item 2", amount: 5, for: list1)
manager.saveItem(name: "Preview item 3", amount: 2, for: list2)
manager.saveItem(name: "Preview item 4", amount: 5, for: list2)
return manager
}()
}
In this instance, we create an initial list and items to use in our previews, please open ShoppingListView
and ShoppingListItemsView
then update the environment object supply method with the following:
// No need shared instance anymore.
// .environmentObject(PersistenceManager.shared)
.environmentObject(PersistenceManager.preview)
From now on, we don't need shared instance of PersistentManager
anymore, open PersistentManager.swift
and remove this shared instance:
final class PersistenceManager: ObservableObject {
// Delete line below
// static let shared = PersistenceManager()
let container: NSPersistentContainer
...
}
Providing Mock Data
Let's provide mock data using our new persistent manager instance for preview, using a generic method:
final class EntityMockDataProvider {
static func mockData<T: NSManagedObject>(for type: T.Type) -> T {
let context = PersistenceManager.preview.container.viewContext
let fetchRequest: NSFetchRequest<T> = NSFetchRequest<T>(entityName: T.entity().name ?? "")
fetchRequest.fetchLimit = 1
let results = try? context.fetch(fetchRequest)
return (results?.first!)!
}
}
This is a nice provider that gets the type of the entity and returns the first item in the preview storage, let's use it in action for our previews:
struct ShoppingListItemsView_Previews: PreviewProvider {
static var previews: some View {
let mockList = EntityMockDataProvider.mockData(for: ShoppingList.self)
return ShoppingListItemsView(shoppingList: mockList)
.environmentObject(PersistenceManager.preview)
}
}
struct ItemView_Previews: PreviewProvider {
static var previews: some View {
let mockData = EntityMockDataProvider.mockData(for: ShoppingListItem.self)
return ItemView(item: mockData)
}
}
Conclusion
SwiftUI previews are extremely powerful to see a view in live mode, but managing previews with local storage dependencies might be tough. There are some practices such as preview instances of local storage to avoid entity object creation for previews and providing mock data for testing purposes. You can visit the final project from here and reach me out to if you have better/similar approaches.