Inspection Structure Fails with Xcode 16
Running the Xcode 16 Beta 6, and the 0.10.0 branch of ViewInspector
View queries that worked in Xcode 15 & 0.9.x release no longer work.
let view = MySwiftUIView()
let shape = try view.inspect().hStack().shape(0)
// error - hStack() found MyModule.MySwiftUIView instead of HStack
Hey, this sounds like a known issue with swift 6 compiler - it started inserting AnyView at random, which changes absolute paths to target views. It's still unclear if this will get released like that, but I highly recommend refactoring your tests to use find instead of absolute paths
Doesn’t this render much of the API useless? How would I replace this with the find API?
I tried find(ViewType.HStack.self).shape(0) but got a similar error
Ah. As I followed the threads you linked, I now understand.
I appreciate all the time you’ve spent researching and trying to fix this. I respect the situation you’re in from the changes from Apple
I had to add .anyView() after .inspect() in most of cases, I would wait for the stable version of Xcode 16 to recheck my fixes.
@A30008910-wei I haven't updated the docs yet, but there is a new special call .implicitAnyView() exactly for this. It gives better clarity in the tests, giving insight that it's not your AnyView you should expect in your view, and also this call is compiler aware, where it'd add attempt to unwrap AnyView for swift 6 compiler, and do nothing for swift 5 compiler
@nalexn since it depends on the SDK version, not the compiler version, the check should be #if canImport(Swift, _version: 6.0), instead.
BTW, I have a view that requires to go with try view.inspect().anyView().anyView().anyView().anyView().vStack() while it is fine with view.inspect().vStack() on Xcode 15. So, I feel I just have to use find() regardless.
@nalexn we need MultipleViewContent version of .implicitAnyView(), too.
@nh7a the AnyView has been updated to conform to MultipleViewContent, you should be able to do .implicitAnyView().someView(1). If this doesn't help - please let me know, and provide more context with an example
I realized implicitAnyView returns untyped view, so that trick won't work. I guess use .anyView() in these cases, that is .anyView().someView(1)
@nalexn this is my situation:
let sut = try view.inspect()
let hStack = try sut.hStack(4)
#if canImport(Swift, _version: 6.0)
try hStack.anyView(0).divider(0) // works for 16.0
#else
try hStack.divider(0) // works for Xcode 15.4
#endif
The below doesn't compile:
try hStack.anyView().divider(0) // Referencing instance method 'anyView()' on 'InspectableView' requires that 'ViewType.VStack' conform to 'SingleViewContent'
try hStack.implicitAnyView().divider(0) // Referencing instance method 'implicitAnyView()' on 'InspectableView' requires that 'ViewType.VStack' conform to 'SingleViewContent'
To support both Xcode 15.4 & 16.0, I think we want to be able to write:
try hStack.implicitAnyView(0).divider(0)
And the below works fine (as I confirmed with my local branch... was what I thought but maybe not; I should make it sure later):
public extension InspectableView where View: MultipleViewContent {
#if canImport(Swift, _version: 6.0)
func implicitAnyView(_ index: Int) throws -> InspectableView<ViewType.AnyView> {
anyView(index)
}
#else
func implicitAnyView(_ index: Int) throws -> InspectableView<View> {
self
}
#endif
}
I am seeing the same problem with ButtonStyle tests. The .inspect(isPressed:) function returns an AnyView which breaks all my tests. I have tried the suggestions in this issue without any success. At this point I was forced to "comment out" all my ButtonStyle tests so I was able to prepare for the iOS 18 release. Here is an example that shows what's happening.
func testStyle() throws {
struct TestStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label.background( configuration.isPressed ? Color.red : Color.blue)
configuration.label.font(.headline)
}
}
let test = TestStyle()
let sut = try test.inspect(isPressed: false)
XCTAssertEqual(try sut.background().color().value(), Color.blue)
XCTAssertEqual(try sut.font(), Font.headline)
}
@Rob-2024 I'm not totally sure if it's worth keeping up in this way, but the code below can test and pass, at least. I hope there's a way to absorb this by ViewInspector itself.
func testStyle() throws {
struct TestStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label.background( configuration.isPressed ? Color.red : Color.blue)
configuration.label.font(.headline)
}
}
let test = TestStyle()
let sut = try test.inspect(isPressed: false)
XCTAssertEqual(try sut.anyView().styleConfigurationLabel(0).background().color().value(), Color.blue)
XCTAssertEqual(try sut.anyView().styleConfigurationLabel(1).font(), Font.headline)
}
I have a similar problem: group() found AnyView instead of Group. Any suggestion? Im using ViewInspector v0.10.0 Xcode 16 Build for swift 5
This is my code:
@MainActor
func testTruncatingReview_NoManagerResponse() throws {
let review = Review(reviewDate: nil, firstName: nil, lastInitial: nil, review: "bla bla bla", overall: 4, response: nil, unitId: nil, reservationId: nil)
let reviewView = GuestReviewView(review: review)
let truncationExpectation = reviewView.inspection.inspect(after: 0.1) { view in // HERE IS THE ERROR
XCTAssertTrue(try view.actualView().isTextTruncated)
XCTAssertTrue(try view.actualView().isViewCollapsed)
try view.find(button: .VCShared.SeeMoreButton).tap()
XCTAssertFalse(try view.actualView().isViewCollapsed)
XCTAssertThrowsError(try view.group().vStack(0).vStack(0).find(text: responseText))
try view.group().vStack(0).find(button: .Shared.SeeLessButton).tap()
XCTAssertThrowsError(try view.group().vStack(0).vStack(1).text(1))
XCTAssertThrowsError(try view.group().vStack(0).find(button: Shared.SeeLessButton))
}
ViewHosting.host(view: reviewView)
wait(for: [truncationExpectation], timeout: 1)
let reviewLabel = try reviewView.inspect().group().vStack(0).view(TruncatedTextView.self, 0).vStack().text(0)
XCTAssertEqual(try reviewLabel.string(), review.review)
}
I have exactly the same problem and I was wondering if there's any plan to upgrade the library to fix it as we have more than 5000 tests and it's a hard work to update it all with .anyView() or .implicitAnyView()
Thanks for your work @nalexn !
Per this Mastodon toot, it appears that a number of new AnyView objects have been added to the hierarchy. If you set SWIFT_ENABLE_OPAQUE_TYPE_ERASURE=NO in your build settings, it will revert to the old behavior. Alternatively, I've had success by adding additional .anyView() methods in my calls, though I've had to do trial and error to find the right number of them.
any way to add SWIFT_ENABLE_OPAQUE_TYPE_ERASURE=NO to swift packages?
Ok, just tried @KatherineInCode 's finding around SWIFT_ENABLE_OPAQUE_TYPE_ERASURE=NO - it works!
It has to be added to the main project's target settings, as it controls how the main target compiles, an external library used for testing cannot control that. It's not obvious where to add that setting, so I figured it magically works when defined as "User-defined" value (which makes it truly a hidden undocumented feature):
I wish I could make func implicitAnyView() to respect this setting but the only way I could find how to read it is through Info.plist, which isn't as straightforward as I would expect. Maybe it's easier to either use implicitAnyView() everywhere or go with SWIFT_ENABLE_OPAQUE_TYPE_ERASURE=NO and forget about this Xcode nonsense
This is the explanation of those AnyView. My question is, if SWIFT_ENABLE_OPAQUE_TYPE_ERASURE is official and reliable.
As of the Xcode 16.2 release notes, this is now officially documented with the workaround of using the SWIFT_ENABLE_OPAQUE_TYPE_ERASURE compiler flag as the setting to revert to the previous behavior.
However, I've yet to find a way to set that flag to NO in Swift Packages - so this only works for your tests that are managed by Xcode
Thanks for sharing this @Mordil . As far as I understand, even if we were able to set the flag in packages - this won't help. Imagine you're importing a library, and hiddenly it changes the compilation mode for the parent project? It works the other way around, your root compilation flags may inherit to child targets, but it cannot transmit between the targets
Hey @Mordil - I am also using the ViewInspector library v0.10.1 and am on Xcode 16.2 - I am trying to run my unit tests locally on Debug build and am getting the following error which I think is related to this issue:
Test XYZ, threw unexpected error: AnyView does not have 'frame(width:)' modifier
This is being thrown by InspectableView.swift func modifierAttribute<Type>(modifierLookup: ...) - InspectionError.modifierNotFound - however, frame modifier is covered by your SDK it seems...
As suggested, I tried setting SWIFT_ENABLE_OPAQUE_TYPE_ERASURE=NO in both my Tests and Project targets, but I'm still seeing this issue. However, instead of pointing to AnyView it points to my concrete MockView:
Test XYZ, threw unexpected error: MockView does not have 'frame(width:)' modifier
Could this be because of what @nalexn is saying regarding setting the flag for the package? Mind you, we import ViewInspector as a CocoaPod, still not really sure how i would set this build setting for the pod
@Germantv please open a separate ticket and provide the essential portion of the body of the view and the test
Xcode 16.3 has reverted that setting, so tests should operate normally