ViewInspector icon indicating copy to clipboard operation
ViewInspector copied to clipboard

Inspection Structure Fails with Xcode 16

Open Mordil opened this issue 1 year ago • 15 comments

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

Mordil avatar Aug 23 '24 17:08 Mordil

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

nalexn avatar Aug 25 '24 14:08 nalexn

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

Mordil avatar Aug 25 '24 14:08 Mordil

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

Mordil avatar Aug 25 '24 14:08 Mordil

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 avatar Sep 06 '24 03:09 A30008910-wei

@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 avatar Sep 06 '24 06:09 nalexn

@nalexn since it depends on the SDK version, not the compiler version, the check should be #if canImport(Swift, _version: 6.0), instead.

nh7a avatar Sep 06 '24 18:09 nh7a

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.

nh7a avatar Sep 06 '24 19:09 nh7a

@nalexn we need MultipleViewContent version of .implicitAnyView(), too.

nh7a avatar Sep 10 '24 14:09 nh7a

@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 avatar Sep 10 '24 15:09 nalexn

@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
}

nh7a avatar Sep 10 '24 16:09 nh7a

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 avatar Sep 17 '24 15:09 Rob-2024

@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)
}

nh7a avatar Sep 17 '24 16:09 nh7a

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)
    }

FelipeUgarte avatar Sep 17 '24 18:09 FelipeUgarte

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 !

christiancabarrocas avatar Oct 16 '24 09:10 christiancabarrocas

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.

KatherineInCode avatar Oct 17 '24 17:10 KatherineInCode

any way to add SWIFT_ENABLE_OPAQUE_TYPE_ERASURE=NO to swift packages?

Weizzz avatar Oct 30 '24 03:10 Weizzz

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):

_erasure_

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

nalexn avatar Nov 02 '24 10:11 nalexn

This is the explanation of those AnyView. My question is, if SWIFT_ENABLE_OPAQUE_TYPE_ERASURE is official and reliable.

nh7a avatar Nov 04 '24 19:11 nh7a

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.

Mordil avatar Dec 19 '24 01:12 Mordil

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

Mordil avatar Dec 19 '24 01:12 Mordil

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

nalexn avatar Dec 19 '24 06:12 nalexn

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 avatar Feb 20 '25 08:02 Germantv

@Germantv please open a separate ticket and provide the essential portion of the body of the view and the test

nalexn avatar Feb 20 '25 08:02 nalexn

Xcode 16.3 has reverted that setting, so tests should operate normally

nalexn avatar Apr 03 '25 16:04 nalexn