Xcode 12 - SwiftUI preview doesn't work on Swift Package when have another Swift Package as dependencies - 'previewInstances' message to agent
Asked Answered
H

5

20

I have an issue on SwiftUI preview in a view located in a Swift Package when my code imports a control or value from an other swift package.

import Foundation
import SwiftUI
import Common

struct AppointmentListItem: View {
    var appointment: Appointment
    var body: some View {
        VStack{
            HStack(spacing: 10){
               //Client Info
                Image(self.appointment.client.profilePicture)
                   .resizable()
                   .aspectRatio(contentMode: .fill)
                   .frame(width: 35, height: 35)
                   .clipShape(Circle())
                   .shadow(radius: 10)
                   .overlay(Circle().stroke(Color.white, lineWidth: 1.5))
                Text(self.appointment.client.fullName)
                   .font(.system(size: 18))
                   .bold()
                   .frame(maxWidth: .infinity, alignment: .leading)
                Text(self.appointment.getHourAndMinutes()).bold()
               //Detail info
               Button(action: {
                   withAnimation{
                       print("Go to details")
                   }
               }){
                   Image(systemName: "ellipsis")
                       .font(.system(size: 18))
                       .frame(width: 20, height: 20)
                    .rotationEffect(Angle.init(degrees: 90))
               }
            }
            .padding()
        }
        .foregroundColor(Color.white)
        .background(RoundedRectangle(cornerRadius: 20)
                        .fill(Color.hippoPrimary)// <- this color is part of Common package
        )
    }
}

If I remove or change .fill(Color.hippoPrimary) the preview is available.

The error provided by Xcode is the following:

RemoteHumanReadableError: Failed to update preview.

The preview process appears to have crashed.

Error encountered when sending 'previewInstances' message to agent.

==================================

|  RemoteHumanReadableError: The operation couldn’t be completed. (BSServiceConnectionErrorDomain error 3.)
|  
|  BSServiceConnectionErrorDomain (3):
|  ==BSErrorCodeDescription: OperationFailed

This is my Package.swift file:

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "TodayAppointments",
    platforms: [
        .iOS(.v13)
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "TodayAppointments",
            targets: ["TodayAppointments"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(path: "../Common")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "TodayAppointments",
            dependencies: ["Common"]),
        .testTarget(
            name: "TodayAppointmentsTests",
            dependencies: ["TodayAppointments"]),
    ]
)

In the Common Package, the Colors are defined this way:

public extension Color {
    static let hippoPrimary = Color("Primary", bundle: .module)
    static let progressBarBackground = Color("ProgressBarBackground", bundle: .module)
    static let textBackground = Color("TextBackground", bundle: .module)
    static let textColor = Color("TextColor", bundle: .module)
    static let appleSignInBackground = Color("AppleSignInBackground", bundle: .module)
    static let buttonActionText = Color("Text", bundle: .module)
}

The build hasn't errors so I understand that the dependencies are ok, sounds like a IDE.

Thanks in Advance.

Halvah answered 26/10, 2020 at 15:26 Comment(0)
D
16

Workaround for both iOS and macOS (not tested with Catalyst):

extension Foundation.Bundle {
    static var swiftUIPreviewsCompatibleModule: Bundle {
        final class CurrentBundleFinder {}

        /* The name of your local package, prepended by "LocalPackages_" for iOS and "PackageName_" for macOS. You may have same PackageName and TargetName*/
        let bundleNameIOS = "LocalPackages_TargetName"
        let bundleNameMacOs = "PackageName_TargetName"

        let candidates = [
            /* 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: CurrentBundleFinder.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: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent(),
            Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
        ]

        for candidate in candidates {
            let bundlePathiOS = candidate?.appendingPathComponent(bundleNameIOS + ".bundle")
            let bundlePathMacOS = candidate?.appendingPathComponent(bundleNameMacOs + ".bundle")

            if let bundle = bundlePathiOS.flatMap(Bundle.init(url:)) {
                return bundle
            } else if let bundle = bundlePathMacOS.flatMap(Bundle.init(url:)) {
                return bundle
            }
        }

        fatalError("unable to find bundle")
    }
}

Then replace calls to Bundle.module with Bundle.swiftUIPreviewsCompatibleModule.

Dextrorotation answered 19/1, 2021 at 10:6 Comment(2)
Which Target association should this code have? On the testing target?Rinehart
This didn't work for me. Ended up just using hex string colors and base64 encoded images.Jodoin
A
4

Update 1:

Sorry my last post was not accurate. This is a bug in Xcode with no work around. Just submitted to Apple a bug report (FB8880328). Also, posted a details write up with example code w/ repro steps here. Direct link to GitHub project: https://github.com/ryanholden8/SwiftUI-Preview-Failing-Test-Project

Old Post:

Got this exact error doing the same thing, putting colors in a separate package. This post helped me get to the bottom of it. I deleted the default class that is generated in the colors package. However, I did not delete the unit test that was based on that default class.

In short: Delete the auto-generated unit test in your Common package. Or make sure all the unit tests pass.

Archetype answered 3/11, 2020 at 14:24 Comment(2)
Thanks @ryanholden8, do you have any news from the apple team?Halvah
@CarlosRodrigo No unfortunately, the status is still "Open" which just means it's assigned to an engineering team to look at, at some point.Archetype
D
1

Here's my solution for Xcode 15.3, which combines a few different techniques I found on here and elsewhere.

Works for iOS and Mac Catalyst simulator devices.

import Foundation

public extension Bundle {
    static let myPackage: Bundle = .swiftUIPreviewsCompatibleModule
}

private extension Bundle {
    private static let packageName = "my-package"
    private static let moduleName = "MyModule"

    static var swiftUIPreviewsCompatibleModule: Bundle {
        final class CurrentBundleFinder {}

        let isPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"

        guard isPreview else {
            // Not a preview, so use regular ol' Bundle.module

            return .module
        }

        let bundleName = "\(packageName)_\(moduleName).bundle"
        let simulatorProductsDirectory = Bundle(for: CurrentBundleFinder.self)
            .resourceURL?
            .deletingLastPathComponents(upToPathComponentWithPrefix: "Debug-")
            .appendingPathComponent(bundleName)

        if let bundleURL = simulatorProductsDirectory,
           let bundle = Bundle(url: bundleURL)
        {
            return bundle
        }

        // Welp. This will likely crash.

        return .module
    }
}

private extension URL {
    func deletingLastPathComponents(upToPathComponentWithPrefix prefix: String) -> URL {
        var currentURL = self

        while let lastPathComponent = currentURL.pathComponents.last, !lastPathComponent.hasPrefix(prefix) {
            currentURL.deleteLastPathComponent()
        }

        return currentURL
    }
}
Deafening answered 18/4 at 16:49 Comment(1)
worked for me with xcode 15.2, but I had to drop the public and make the first extension internalGeniculate
V
0

This worked for me in Xcode 14.2

private extension Bundle {
    private static let packageName = "PACKAGE_NAME"
    private static let moduleName = "MODULE_NAME"
    
    #if targetEnvironment(simulator)
    static var swiftUIPreviewsCompatibleModule: Bundle {
        final class CurrentBundleFinder {}

        let isPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
        
        guard isPreview else {
            return Bundle.module
        }
        
        // This is a workaround for SwiftUI previews
        // previews crash when accessing other package view using assets from Bundle.module
        
        let bundleName = "\(packageName)_\(moduleName).bundle"
        
        func bundle(stepsBack: Int) -> Bundle? {
            var bundleURL = Bundle(for: CurrentBundleFinder.self).bundleURL
            for _ in 1...stepsBack { bundleURL.deleteLastPathComponent() }
            bundleURL.appendPathComponent(moduleName)
            bundleURL.appendPathComponent("Products")
            bundleURL.appendPathComponent("Debug-iphonesimulator")
            bundleURL.appendPathComponent("PackageFrameworks")
            
            let directories: [String]
            do {
                directories = try FileManager.default.contentsOfDirectory(atPath: bundleURL.path)
            } catch {
                return nil
            }
            
            guard let matchingDir = directories.first(where: { $0.hasSuffix(".framework") }) else {
                return nil
            }
            
            bundleURL.appendPathComponent(matchingDir)
            bundleURL.appendPathComponent(bundleName)
            
            return Bundle(url: bundleURL)
        }
        
        // Steps back 5 is a workaround for crashes
        // when another module is importing this module
        return bundle(stepsBack: 5) ?? .module
    }
    #else
    static var swiftUIPreviewsCompatibleModule: Bundle { .module }
    #endif
}
Vitrics answered 8/1, 2023 at 22:40 Comment(0)
A
0

I'm using Xcode 13.4.1

I tried to use this line as the answer above mentioned but it did not work as it can not find the bundle:

/* The name of your local package, prepended by "LocalPackages_" for iOS and "PackageName_" for macOS. You may have same PackageName and TargetName*/
let bundleNameIOS = "LocalPackages_TargetName"

My package is called "Networking". After some debugging, I changed the bundle name as follows and it worked perfectly!

let bundleNameIOS = "Networking_Networking"

Maybe this helps anyone!

Aqaba answered 10/1, 2023 at 16:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.