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 MainAppTests
→ UIComponentsTestSupport
→ UIComponents
route, and the other via MainApp
→ UIComponents
.
➜ ~ 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!