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.

SwiftPM + CoreData: Failing SwiftUI Previews? Here Are 5 Tips to Fix
Photo by James Wainscoat / Unsplash

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 Utilityinside 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:

Error stating `FocusTimer` can’t be found in scope despite `import Model` & successful build.

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 Diagnosticsbutton when SwiftUI previews fail:

Clicking on “Diagnostics” just stating “MessageError: Connection interrupted”.

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:

Contents of Xcode Previews .ips file with a proper error console output.

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 the PreviewProvider) 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 the packageName and targetName 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!

👤
Want to Connect?
Follow me also on 👾 Twitch, on 🎬 YouTube and on 🐦 Twitter.