Hello,
We have recently came across a very strange issue related to Swift Packages and Xcode, which also involves Generics
I was able to recreate a minimum reproducible example of the issue we are encountering which I will use the describe the issue we have
I created a 'New Project' using Xcode 14.3 and have configured it with unit tests.
In the new project I have added a local Swift Package 'MyLibrary' (default name)
as you can see below
Package.swift
let package = Package(
name: "MyLibrary",
products: [
.library(
name: "MyLibrary",
targets: ["MyLibrary"]
),
.library(
name: "MyLibraryMocks",
targets: ["MyLibraryMocks"]
),
],
dependencies: [],
targets: [
.target(
name: "MyLibrary",
dependencies: []
),
.target(
name: "MyLibraryMocks",
dependencies: [
"MyLibrary"
]
)
]
)
The package is made up of a "main" target called 'MyLibrary', and a mocks target called 'MyLibraryMocks' which has a dependency on the MyLibrary
target. MyLibraryMocks
is intended to be used only for unit testing, so it will not be included in the main application target, but rather in the unit tests target of the Xcode project.
MyLibrary.swift
contains a simple 1:1 relationship between a 'Component' protocol and its 'Model' counterpart
MyLibrary.swift
public protocol Component<M> where M.C == Self {
associatedtype M: Model
}
public protocol Model<C>: Hashable where C.M == Self {
associatedtype C: Component
}
public final class A: Component {
public typealias M = B
public init() { }
}
public struct B: Model {
public typealias C = A
public init() { }
}
MyLibraryMocks.swift
contains mock implementations for both the Component
and the Model
protocols
MyLibraryMocks.swift
import MyLibrary
public struct Mock: Component {
public typealias M = MockModel
public init() { }
}
public struct MockModel: Model {
public typealias C = Mock
public init() { }
}
I have added MyLibrary
as a dependency in the main application target and have created and ComponentAdapter
class which follows the Adapter design pattern.
ComponentAdapter.swift
final class Adapter {
func adapt() -> some Model {
B()
}
}
As you can see, Adapter.adapt
method returns an opaque implementation of the Model
protocol, because we are really not interested in the actual type that is returned, we only care that it conforms to the Model
protocol. This gives us the flexibility to change the object returned by the method, in the future, without breaking the code of the consumer, as the actual type is only an implementation detail.
In the project's unit tests target, we have added MyLibraryMocks
as a dependency, since it is not included in the application's target.
Strangely enough, when testing the Adapter
class, we are not able to cast the opaque some Model
returned by the adapt
method to its actual implementation, even though we know for sure what the actual type is
// AdapterTests.swift
import XCTest
@testable import MyLibrary
@testable import MyLibraryMocks
@testable import MyApp
final class AdapterTests: XCTestCase {
func testExample() throws {
let expectedModel = B()
let adapter = Adapter()
let model = adapter.adapt() as! B // Thread 1: signal SIGABRT
XCTAssertEqual(expectedModel, model)
}
}
Running the above test actually crashes at runtime and we are left completely clueless on why that is so.
Xcode console shows the following output
objc[33111]: Class _TtC9MyLibrary1A is implemented in both /Users/victorsocaciu/Library/Developer/XCTestDevices/D7379CE3-8129-4E21-81DC-D7F4B586372F/data/Containers/Bundle/Application/35261BB7-60EF-4706-B3D7-3178E6E40D1E/MyApp.app/MyApp (0x1005555b0) and /Users/victorsocaciu/Library/Developer/XCTestDevices/D7379CE3-8129-4E21-81DC-D7F4B586372F/data/Containers/Bundle/Application/35261BB7-60EF-4706-B3D7-3178E6E40D1E/MyApp.app/PlugIns/MyAppTests.xctest/MyAppTests (0x1018cc2d0). One of the two will be used. Which one is undefined.
Test Suite 'AdapterTests' started at 2023-04-21 14:04:45.614
Test Case '-[MyAppTests.AdapterTests testExample]' started.
Could not cast value of type 'MyLibrary.B' (0x100550338) to 'MyLibrary.B' (0x1018c8280).
2023-04-21 14:04:45.614851+0400 MyApp[33111:22124448] Could not cast value of type 'MyLibrary.B' (0x100550338) to 'MyLibrary.B' (0x1018c8280).
While still connected to the debugger, when running the following command in the console po model as! B
, the debugger doesn't crash and it actually prints the correct output MyLibrary.B()
the following test also fails
func testExample2() throws {
let expectedModel = B()
let adapter = Adapter()
let model = adapter.adapt()
let modelType = type(of: model)
print(modelType) // B
XCTAssertEqual(
ObjectIdentifier(B.self),
ObjectIdentifier(modelType)
) // testExample2(): XCTAssertEqual failed: ("ObjectIdentifier(0x00000001037982c0)") is not equal to ("ObjectIdentifier(0x00000001025b8338)")
}
Has anyone else faced this before? Are we misusing the Swift Packages somehow?
EDIT:
I have uploaded the sample project to github GitHub - vykut/SwiftPackageDuplicateSymbols