Duplicate symbols across app and test targets — runtime cast from `MyView` to `MyView` fails

We have a simple demo project setup using SPM with the following dependency graph:

Inside the UIComponents package we have a UIView subclass that's loaded from a XIB file:

public class MyView: UIView {
    public static func instantiateFromNib() -> MyView {
        let nib = UINib(nibName: String(describing: self), bundle: .module)
        let objects = nib.instantiate(withOwner: nil)
        return objects.first as! Self
    }
}

The test support package mirrors similar ones we have in production where various types and values from the UIComponents library are instantiated with common test values that other test targets can utilise.

Loading the view from the XIB inside the main app works fine at runtime:

class ViewController: UIViewController {
    let view: MyView = .instantiateFromNib()
    ...
}

However we hit problems when executing the following test inside the app's test target:

@testable import MainApp
import UIComponents
import UIComponentsTestSupport

final class MainAppTests: XCTestCase {
    func testExample() throws {
        let view = MyView.instantiateFromNib()
        let testViewDatas = MyViewTestSupport.testViewDatas
        ....
    }
}

Here we get a Thread 1: signal SIGABRT at objects.first as! Self within instantiateFromNib():

Could not cast value of type 'UIComponents.MyView' (0x1002752d8) to 'UIComponents.MyView' (0x129eb0348).

You can see that there's 2 separate memory locations for UIComponents.MyView and hence 2 symbols that are available.

The main app is only linked to the UIComponents library, and the test target is linked to both the UIComponents and UIComponentsTestSupport libraries (although the error remains if the test target is only linked to UIComponentsTestSupport). It also still happens if we remove the @testable import MainApp.

There looks to be 2 routes where the symbol is available, one via MainAppTestsUIComponentsTestSupportUIComponents route, and the other via MainAppUIComponents.

➜ ~ nm -g .../Build/Products/Debug-iphonesimulator/MainApp.app/MainApp | grep MyView
00000001000112d8 S _OBJC_CLASS_$__TtC6UIComponents8MyView
0000000100011678 D _OBJC_METACLASS_$__TtC6UIComponents8MyView
➜ ~ nm -g .../Build/Products/Debug-iphonesimulator/MainApp.app/PlugIns/MainAppTests.xctest/MainAppTests | grep MyView
000000000000c348 S _OBJC_CLASS_$__TtC6UIComponents8MyView
000000000000c540 D _OBJC_METACLASS_$__TtC6UIComponents8MyView

I'm unsure of how and when each one seems to be used, and which one is used when UIKit internally instantiates the view from the XIB. I'm also unsure how this conflict can be avoided or worked-around.

Here's the full Package.swift:

let package = Package(
    name: "MyPackage",
    products: [
        .library(
            name: "UIComponents",
            targets: ["UIComponents"]
        ),
        .library(
            name: "UIComponentsTestSupport",
            targets: ["UIComponentsTestSupport"]
        ),
    ],
    dependencies: [],
    targets: [
        .target(
            name: "UIComponents",
            dependencies: []
        ),
        .target(
            name: "UIComponentsTestSupport",
            dependencies: ["UIComponents"]
        ),
    ]
)

I'm quite out of ideas how this setup (i.e. library + test support library) can be used in this quite simple project, so any information on how to further diagnose and overcome this issue would be greatly appreciated!

Packages product static libraries by default. So that means you have two static libraries with different symbols (but the same "names" as you would see them) involved here. Can you make UIComponents a dynamic library?

Sadly setting UIComponents, or even both libs, as dynamic doesn't fix the issue.

When setting the product to be .dynamic:

.library(
    name: "UIComponents",
    type: .dynamic,
    targets: ["UIComponents"]
),

You still see the view class symbol make its way into the test app executable, via the statically linked test support library:

➜ ~ nm -g .../Build/Products/Debug-iphonesimulator/MainApp.app/PlugIns/MainAppTests.xctest/MainAppTests | grep MyView
000000000000c348 S _OBJC_CLASS_$__TtC6UIComponentsI8MyView
000000000000c540 D _OBJC_METACLASS_$__TtC6UIComponents8MyView

Which duplicates the symbols that exist in the in the dynamic framework UIComponents.framework:

➜ ~ nm -g .../Build/Products/Debug-iphonesimulator/MainApp.app/Frameworks/UIComponents.framework/UIComponents | grep MyView
000000000000c370 S _OBJC_CLASS_$__TtC6UIComponents8MyView
000000000000c4b0 D _OBJC_METACLASS_$__TtC6UIComponents8MyView

So there's still duplication and the same error occurs.

If we also make the test support lib dynamic, the view symbols are instead duplicated in UIComponentsTestSupport.framework:

➜ ~ nm -g .../Build/Products/Debug-iphonesimulator/MainApp.app/PlugIns/MainAppTests.xctest/Frameworks/UIComponentsTestSupport.framework/UIComponentsTestSupport | grep MyView
000000000000c400 S _OBJC_CLASS_$__TtC6UIComponents8MyView
000000000000c5d0 D _OBJC_METACLASS_$__TtC6UIComponents8MyView

It seems that because the test support target has the UIComponents target as a dependency:

.target(
    name: "UIComponentsTestSupport",
    dependencies: ["UIComponents"]
),

Somehow those symbols from the UIComponents target are making their way into the test support product, instead of being "undefined" and resolved at runtime using the dynamic UIComponents product.

It very much makes sense that dynamic should be answer, but we cannot get this setup working with SPM at all.

This is expected, as targets cannot depend on products from the same package. So the "UIComponents" dependency is on the target, not the dynamic product.

@NeoNacho That makes sense, and therefore UIComponentsTestSupport lib would need to live in a separate package to UIComponents so the target can have a "dynamic" dependency:

...
dependencies: [
    .package(name: "UIComponents", path: "../UIComponents")
],
targets: [
    .target(
        name: "UIComponentsTestSupport",
        dependencies: [.product(name: "UIComponents", package: "UIComponents")]
    ),
]
...

However this means the unit tests for UIComponents cannot live inside the UIComponents package, because it will need to also use the test support package and there would be a cyclic package dependency.

If UIComponents becomes is dynamic and I move the UIComponentsTests tests to another package then we see a bundle resolution error when the test target (in another package) tries to load the view from the XIB:

fatalError("unable to find bundle named UIComponents_UIComponents")

In the list of candidate paths from within the Bundle.module implementation, the following is listed:

.../Build/Products/Debug-iphonesimulator/PackageFrameworks/UIComponents.framework/UIComponents_UIComponents.bundle

However the resource bundles are only located on the following paths:

.../Build/Products/Debug-iphonesimulator/UIComponentsTests.xctest/UIComponents_UIComponents.bundle
.../Build/Products/Debug-iphonesimulator/UIComponents_UIComponents.bundle

It seems all combinations lead to errors and I can't see any way to have a lib containing resources together with a test support lib using SPM. I'd really appreciate some guidance so we're not blocked from modularising using SPM.