SwiftPM + CoreData: Failing SwiftUI Previews? Here Are 5 Tips to Fix
Fixing Xcode bugs that make SwiftUI previews fail in apps modularized with SwiftPM and that are using CoreData.
My SwiftUI previews didn’t work properly since the day I had set up the project for the Open Focus Timer in Xcode using Point-Free’s modularization approach — with the CoreData checkbox enabled to get a good starting point for my model layer. This was quite annoying, after all getting faster builds and therefore more reliable SwiftUI previews was one of the main reasons I had opted to modularize my app into small chunks in the first place.
So in one of my streams (this is an open-source app I am developing fully in the open while streaming live on Twitch) I decided to tackle this problem and fix the SwiftUI preview error once and for all. And I failed:
Thanks to some help from the great Swift community on Twitter, I could figure out the root cause of the issue: SwiftUI previews get into trouble when CoreData models are referenced in them.
But while I thought that it’s just a path issue that can be fixed with a simple workaround, it was not as simple as that. Yes, there is a path issue involved, but while solving the previews, I came across multiple levels of failure. And I learned how to debug SwiftUI previews along the way. Let me share my learnings…
#1: Explicit Dependencies in Package Manifest
First things first. Using Point-Free’s modularization approach means you’ll have a Package.swift
file to manage manually. For each module, you’ll add a target
, a testTarget
and a library
entry and for each target, you’ll need to specify the dependencies. Xcode does not help here in any way other than recognizing the changes you make in that file. With many packages, the manifest file can grow significantly, and there’s currently no help I’m aware of to make this easier. This is what my manifest looks like right now:
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "OpenFocusTimer",
defaultLocalization: "en",
platforms: [.macOS(.v12), .iOS(.v15)],
products: [
.library(name: "AppEntryPoint", targets: ["AppEntryPoint"]),
.library(name: "Model", targets: ["Model"]),
.library(name: "TimerFeature", targets: ["TimerFeature"]),
.library(name: "ReflectionFeature", targets: ["ReflectionFeature"]),
.library(name: "Resources", targets: ["Resources"]),
],
dependencies: [
// Commonly used data structures for Swift
.package(url: "https://github.com/apple/swift-collections", from: "1.0.2"),
// Handy Swift features that didn't make it into the Swift standard library.
.package(url: "https://github.com/Flinesoft/HandySwift", from: "3.4.0"),
// Handy SwiftUI features that didn't make it into the SwiftUI (yet).
.package(url: "https://github.com/Flinesoft/HandySwiftUI", .branch("main")),
// ⏰ A few schedulers that make working with Combine more testable and more versatile.
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.5.3"),
// A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.33.1"),
// Safely access Apple's SF Symbols using static typing Topics
.package(url: "https://github.com/SFSafeSymbols/SFSafeSymbols", from: "2.1.3"),
],
targets: [
.target(
name: "AppEntryPoint",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "HandySwift", package: "HandySwift"),
.product(name: "HandySwiftUI", package: "HandySwiftUI"),
"Model",
"ReflectionFeature",
"TimerFeature",
"Utility",
]
),
.target(
name: "Model",
dependencies: [
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "HandySwift", package: "HandySwift"),
.product(name: "SFSafeSymbols", package: "SFSafeSymbols"),
],
resources: [
.process("Model.xcdatamodeld")
]
),
.target(
name: "TimerFeature",
dependencies: [
.product(name: "HandySwift", package: "HandySwift"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
"Model",
"ReflectionFeature",
"Resources",
.product(name: "SFSafeSymbols", package: "SFSafeSymbols"),
"Utility",
]
),
.target(
name: "ReflectionFeature",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "HandySwift", package: "HandySwift"),
"Model",
"Resources",
"Utility",
]
),
.target(
name: "Resources",
resources: [
.process("Localizable")
]
),
.target(
name: "Utility",
dependencies: [
.product(name: "CombineSchedulers", package: "combine-schedulers"),
"Model",
]
),
.testTarget(
name: "ModelTests",
dependencies: ["Model"]
),
.testTarget(
name: "TimerFeatureTests",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
"TimerFeature",
]
),
]
)
The problem with managing this file manually isn’t just the manual work. Xcode seems to behave inconsistently regarding the dependencies: When you do a normal build targeting the Simulator for example, a dependency of a dependency seems to get automatically linked to your target. So if my TimerFeature
is importing Utility
for example, but it’s not listed as a dependency under the TimerFeature
target, Xcode might still be able to compile without errors if another dependency, e.g. Model
also depends on Utility
so Xcode can indirectly access Utility
inside of TimerFeature
because TimerFeature
is listing Model
as its dependency.
While this sounds very useful, it can become quite frustrating because SwiftUI previews work differently. For them, as far as I can tell, this transitive kind of implicit imports don’t work. The same seems to be true for running tests as well (at least sometimes). In other words: It’s important to always double-check the dependencies
for each target and not to forget to add every import
you make in a target to the related target in your Package.swift
manifest file.
Maybe, someone will write a tool to help make this easier in the future. 🤞
#2: Generated Code not reliably picked up by Xcode
Another issue I had come across was that even when my builds succeeded, Xcode would (after showing me the “Build succeeded” dialog) show an error in the editor within PreviewProvider
stating it can’t find FocusTimer
:
While not necessarily a blocker, this made me feel the SwiftUI previews might also fail due to the generated code. To fix this, I opted for asking Xcode to generate the code files once and adding them to my packages explicitly. This can be done by opening the .xcdatamodel
file, then clicking Editor
and choosing Create NSManagedObject Subclass...
:
Note that you will need to delete and re-create these generated files each time you make a change to the model (which you should do rarely anyways to prevent database migration problems). Additionally, select the model in Xcode and set Codegen
to Manual/None
.
With this done, the editor no longer shows an error.
#3: SwiftUI Diagnostics != SwiftUI Crash Reports
Here’s a learning for those (like me) wondering how to make use of errors like this after pressing the Diagnostics
button when SwiftUI previews fail:
How is this error message supposed to help us, it’s not very useful:
Message send failure for send previewInstances message to agent
====
MessageError: Connection interrupted
To get more details, read the small gray text below the title of the modal:
Use “Generate Report” to create information that can be sent to Apple to diagnose system problems.
This hint is quite misleading. It sounds like this step is only useful to help Apple analyze the problem. But we can use it, too! Just click the “Generate Report” button and select “Reveal in Finder” in the dropdown. Then Xcode will generate a report and open the Finder app with the generated folder highlighted like this:
Viewing the contents of the highlighted folder will reveal many files that hold different kinds of details about the SwiftUI preview build. The most useful file for debugging lies inside the folder CrashLogs
where you can find one or multiple .ips
files that we can easily open in Xcode via a double-click:
The contents of this file look much more like the error outputs we get in Xcode's console when builds fail, including the very reason the build failed and even a stack of calls that happened at the time of failure. It states:
Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[FocusTimer running]: unrecognized selector sent to instance 0x12886cac0’
Now we have a place we can start debugging and we know that for some reason SwiftUI previews could not access the running
property of our FocusTimer
model. This was key to making the connection to CoreData, otherwise I would have to wild guess why the previews were failing.
I think Xcode should just show this stack trace in the Diagnostics screen right away, this would have helped me save some time. Maybe in Xcode 14? 🤞
#4: In-Memory ManagedObjectContext in Mocks
After playing around a little bit with different things, I found the root cause for the unrecognized selector
issue: It was related to how I created my mocked FocusTimer
object for use within the PreviewProvider
:
#if DEBUG
extension FocusTimer {
public static var mocked: FocusTimer { .init() }
}
#endif
#if DEBUG
struct TimerView_Previews: PreviewProvider {
private static let store = Store(
initialState: TimerState(currentFocusTimer: FocusTimer.mocked), // <-- using it here
reducer: timerReducer,
environment: AppEnv.mocked
)
static var previews: some View {
TimerView(store: self.store).padding()
}
}
#endif
By the way: Yes, I am putting all code related to SwiftUI previews (including thePreviewProvider
) inside#if DEBUG
directives. This ensures I never accidentally call into code that I only wrote for SwiftUI previews in my production code.
I was thoughtlessly calling the .init
method on my FocusTimer
, which is a subclass of NSManagedObject
as I thought that’s the easiest way to initialize an empty FocusTimer
. But there is no init
method on NSManagedObject
, instead NSManagedObject
itself is a subclass of NSObject
and the init() is defined on that level. This does not create a proper CoreData model though, instead we need to call the init(context:) method of NSManagedObject
.
Thankfully, when creating a new Xcode project and enabling the CoreData
checkbox, Xcode creates a PersistenceController
file with an init method that accepts an inMemory: Bool
parameter:
import CoreData
struct PersistenceController {
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CoreDataDemo")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
This is important because we don’t want to create actual databases in our previews (this could cause another error making our previews fail), instead we just want to use an in-memory database which never gets actually persisted (despite the name PersistenceController
).
Note that I had to replace the first line creating the container
with the following 3 lines to make it load the CoreData model from the correct path when extracting the CoreData model code into a separate SwiftPM module:
let modelUrl = Bundle.module.url(forResource: "Model", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelUrl)!
container = NSPersistentContainer(name: "Model", managedObjectModel: managedObjectModel)
Next, I added this mocked
property to the PersistenceController
:
#if DEBUG
public static let mocked = PersistenceController(inMemory: true)
#endif
Now, I adjusted the FocusTimer
mock by calling into the correct init method:
#if DEBUG
extension FocusTimer {
public static var mocked: FocusTimer { .init(context: .mocked) }
}
#endif
This fixed the unrecognized selector
error in SwiftUI previews! 🎉
But it was not over yet, there was one more very weird Xcode bug to fix …
#5: Bundle.module not working in Previews
Lastly, with all the previous steps applied, I came across this error stating:
Fatal error: unable to find bundle named OpenFocusTimer_Model
Thankfully, here the aforementioned pointer of a kind developer in the Swift community on Twitter helped, which pointed me to a thread with this answer on StackOverflow.
It’s basically saying that there’s currently a bug in Xcode (or SwiftPM?) which makes Bundle.module
point to the wrong path in SwiftUI previews. To fix it, they are suggesting to add a Bundle
extension with a custom search. Here’s the full code slightly adjusted to fit my coding & commenting style:
import Foundation
extension Foundation.Bundle {
/// Workaround for making `Bundle.module` work in SwiftUI previews. See: https://stackoverflow.com/a/65789298
///
/// - Returns: The bundle of the target with a path that works in SwiftUI previews, too.
static var swiftUIPreviewsCompatibleModule: Bundle {
#if DEBUG
// adjust these for each module
let packageName = "OpenFocusTimer"
let targetName = "Model"
final class ModuleToken {}
let candidateUrls: [URL?] = [
// Bundle should be present here when the package is linked into an App.
Bundle.main.resourceURL,
// Bundle should be present here when the package is linked into a framework.
Bundle(for: ModuleToken.self).resourceURL,
// For command-line tools.
Bundle.main.bundleURL,
// Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/").
Bundle(for: ModuleToken.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent()
.deletingLastPathComponent(),
Bundle(for: ModuleToken.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
]
// The name of your local package, prepended by "LocalPackages_" for iOS and "PackageName_" for macOS.
let bundleNameCandidates = ["\(packageName)_\(targetName)", "LocalPackages_\(targetName)"]
for bundleNameCandidate in bundleNameCandidates {
for candidateUrl in candidateUrls where candidateUrl != nil {
let bundlePath: URL = candidateUrl!.appendingPathComponent(bundleNameCandidate)
.appendingPathExtension("bundle")
if let bundle = Bundle(url: bundlePath) { return bundle }
}
}
return Bundle.module
#else
return Bundle.module
#endif
}
}
When copy and pasting this code, make sure to adjust thepackageName
andtargetName
variables to your package & target names accordingly.
Note that I wrapped the workaround into an #if DEBUG
to ensure my production code does not accidentally use this path search and instead relies on the official Bundle.module
. Also, I removed the fatalError
from the workaround code found on StackOverflow, so in case it can’t find a Bundle in the custom search paths it doesn’t fail but instead I return Bundle.module
as a fallback. This is supposed to make the code more resilient and continue to work even when this bug gets fixed in a future Xcode release but the custom search paths may no longer work.
Now, the last change I had to make in the PersistenceController
was to replace the call to Bundle.module
with a call to the new Bundle.swiftUIPreviewsCompatibleModule
:
let modelUrl = Bundle.swiftUIPreviewsCompatibleModule.url(forResource: "Model", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelUrl)!
container = NSPersistentContainer(name: "Model", managedObjectModel: managedObjectModel)
And finally, my SwiftUI previews started working again!
A native Mac app that integrates with Xcode to help translate your app.
Get it now to save time during development & make localization easy.