Closed bryankeller closed 5 months ago
@nalexn this is what it looks like in our codebase, to give you a better idea of how this can be used:
extension CollectionViewRepresentable: CustomInspectable {
@ViewBuilder
public var inspectableRepresentation: some View {
ForEach(sections) { section in
ForEach(Array(section.supplementaries.enumerated()), id: \.offset) { _, supplementary in
supplementary._opaqueViewForTesting
}
ForEach(section.items, id: \.id) { item in
item._opaqueViewForTesting
}
}
}
}
@bryankeller thanks for the PR! Just to clarify, there is an API for accessing UIViews
and UIViewControllers
hosted by UIView(Controller)Representable
via .actualView().uiView()
and actualView().viewController()
calls.
You can see examples in the tests.
That gives you access to the UIKit views, but I haven't tested if this is possible to further dig up embedded SwiftUI views from UIKit hierarchy. I assume that should be possible by means of UIKit API (recursive traverse of subviews).
Could you confirm this approach doesn't allow you to achieve what you want?
@bryankeller thanks for the PR! Just to clarify, there is an API for accessing
UIViews
andUIViewControllers
hosted byUIView(Controller)Representable
via.actualView().uiView()
andactualView().viewController()
calls. You can see examples in the tests.That gives you access to the UIKit views, but I haven't tested if this is possible to further dig up embedded SwiftUI views from UIKit hierarchy. I assume that should be possible by means of UIKit API (recursive traverse of subviews).
Could you confirm this approach doesn't allow you to achieve what you want?
Hi @nalexn,
I can provide some additional context here. @bryankeller please feel free to add more info if you spot anything I missed.
Our internal use case is that we need to use UIKit types to accomplish things that SwiftUI still falls short of our requirements (layout/scrolling capabilities, performance, etc).
The key internal implementation detail is that we use the UICollectionViewCell's contentConfiguration
property set to an instance of the UIHostingConfiguration
to host SwiftUI views in a UICollectionView
.
We basically have a SwiftUI View that wraps a UICollectionView
which displays cells containing SwiftUI Views.
The key motivation behind the CustomInspectable
is that we don't want to require the testing code to have knowledge of this key internal implementation detail and how the hierarchy is implemented.
It's just more ergonomic and transparent for the testing code if the parent SwiftUI View to expose the children SwiftUI Views in the cells as its content directly (instead of using UIViewRepresentable
/UICollectionView
) so that the full hierarchy can be easily inspected and traversed in the context of a test using ViewInspector.
We also have some other SwiftUI Views that implement the same "SwiftUI -> UIKit -> SwiftUI" wrapping pattern that are just implemented differently from the key implementation detail I just mentioned above. So the CustomInspectable
is a good solution to normalize these scenarios of "SwiftUI -> UIKit -> SwiftUI" for the tests even if their internal implementation is different.
Thanks @rafael-assis - that description is spot on.
One thing I'd add is that for our use case, we could technically get the underlying collection view via actualView()
and look at its subviews, but then people writing tests would need to know about the internal implementation details of the UIViewRepresentable
in order to figure out where the SwiftUI views are (they're in the cell's contentConfiguration
s via UIHostingConfiguration
). With the approach in this PR, folks can write their tests the same way they'd write them for a List
(which means swapping the implementation from List
to the UIViewRepresentable
UICollectionView
wouldn't require rewriting the tests).
Thanks for elaborating on the topic, I've added this use case to the documentation and merged the PR.
Hey @nalexn ,
At Airbnb, we've recently discovered that
ViewInspector
doesn't work with some of our new SwiftUI infrastructure. We're having trouble getting a customUIViewRepresentable
that contains SwiftUI subviews to properly work. Internally, we've wrappedUICollectionView
in aUIViewRepresentable
, and under the hood, it usesUIHostingConfiguration
s to show SwiftUI views in cells.The API for this
UIViewRepresentable
is pretty declarative - we pass it some data, and under the hood, it diffs the data and updates the collection view.The issue is that
ViewInspector
doesn't really know how to handle aUIViewRepresentable
that contains SwiftUI views. After some discussion internally, we realized that one way around this limitation is to simply expose an alternative SwiftUI-native representation of theUIViewRepresentable
toViewInspector
, based on the current set of data. For example, if we have aUIViewRepresentable
wrapping a collection view, and its current cell data looks like this:We could expose this to
ViewInspector
using a custom view-building property:Internally,
ViewInspector
would use this custom representation, rather than theUIViewRepresentable
. TheinspectableRepresentation
view would likely be aForEach
, rather than a hard-codedVStack
like in my simple example above.The full approach taken is to introduce a protocol called
CustomInspectable
. Any SwiftUI view can conform to this, and if it does,ViewInspector
will use the custom view it provides for inspection, rather than treating it as aUIViewRepresentable
or whatever type it originally was.Other use cases might be:
This approach is a purely additive change, and requires very little additional code in the library. I'd love to get your thoughts on it :)