Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessing properties on a view that takes a generic #294

Open
JOyo246 opened this issue Feb 28, 2024 · 8 comments
Open

Accessing properties on a view that takes a generic #294

JOyo246 opened this issue Feb 28, 2024 · 8 comments

Comments

@JOyo246
Copy link

JOyo246 commented Feb 28, 2024

Considering some view that takes in content

struct WrapperView<Content>: View where Content: View {
    let someProperty: Int
    @ViewBuilder var content: () -> Content

    var body: some View {
        VStack {
            Text("Some property \(someProperty / 2)")
            content()
        }
    }
}

let sut = VStack { 
    WrapperView("Cool") {
        VStack {VStack {VStack {VStack { Text("Some Complicated Content") }}}}
    }
}

We can find a view that is nested in content like so:

let foundWrapper = try sut.find(WrapperView<EmptyView>.self) // succeeds
let foundText = try foundWrapper.find(Text.self) // succeeds

But, now it seems as though it's impossible to access someProperty?

let foundWrapperAV = try foundWrapper.actualView().someProperty // fails with type mismatch

Just want to make sure we are not missing anything. we'd like to be able to get the properties of WrapperView, ignoring the Content generic

@grantneufeld
Copy link

Would casting the result of actualView() to the expected class work?

E.g., something like:

let wrapperView = try foundWrapper.actualView() as? WrapperView<EmptyView>
let foundWrapperAV = wrapperView?.someProperty

(I’m not in front of Xcode to try this out right now, so the code above might not be exact.)

@JOyo246
Copy link
Author

JOyo246 commented Apr 25, 2024

Would casting the result of actualView() to the expected class work?

E.g., something like:

let wrapperView = try foundWrapper.actualView() as? WrapperView<EmptyView>
let foundWrapperAV = wrapperView?.someProperty

(I’m not in front of Xcode to try this out right now, so the code above might not be exact.)

Late follow up, but this doesn't work. The actualView method still throws with a type mismatch. Which makes sense, as the view that is found is something like WrapperView<VStack<...>>

@nalexn
Copy link
Owner

nalexn commented Jun 20, 2024

This is a tricky one. Accessing someProperty the normal way means the compiler needs to know the exact type of its container (including the inner generics), so without explicit cast to that type it won't let you do .someProperty.

There is a hacky way though. Try the following:

@testable import ViewInspector // add @testable so you could use internal methods of ViewInspector

let foundWrapper = try sut.find(WrapperView<EmptyView>.self)
let value = try Inspector.attribute(path: "content|view|someProperty", value: foundWrapper, type: Int.self)

@josh-arnold-1
Copy link

Hey @nalexn ! I have a similar issue!

Given a custom container, how can we inspect the generic property content? In your example, you are using a concrete type Int.

struct CustomContainer<Content: View>: View {
    let content: Content
    // ...
}

I'm thinking, this should be possible right since VStack and HStack, etc have a very similar definition?

// SwiftUI VStack
struct VStack<Content> : View where Content : View {}

Also, is there anyway you could briefly explain the path: argument and how it works? I'm confused in your example by the content|view prefix?

Thanks so much for your time!

@nalexn
Copy link
Owner

nalexn commented Aug 1, 2024

@josh-arnold-1 the internal function Inspector.attribute(path: ) is just a handy wrapper around Swift reflection. The content|view|someProperty means it'll read property with name content, on that value read the property with name view, then someProperty, and finally cast the resulting value to Int.
Usually, this is used for reading private properties of SwiftUI views, but in this example, both content and view are ViewInspector's internal containers, where view references the actual SwiftUI view.
If you want to read the property named differently, like in your example, you'd need to change that last path value, making it content|view|content. I doubt you'll be able to provide the type to cast to though, so instead I'd suggest you use find instead, on the parent view, to locate the view passed as Content.

@josh-arnold-1
Copy link

Thanks a lot for the context! How is the body property of types like VStack resolved in this case? Is it following a similar pattern where you call find on the VStack for the specific child view, and then work your way up the tree?

E.g, something like this?

let view = AnyView(HStack { Text("abc") })
let text = try sut.inspect().find(text: "abc")
let hStack = try text.parent().hStack()
let anyView = try text.parent().parent().anyView()

I'm wondering if the logic for VStack, for example, could somehow be generalized so it also works for custom generic containers like in my example?

Like, I'm wondering what the difference is between these types?

struct CustomContainer<Content>: View where Content : View {}
struct VStack<Content> : View where Content : View {}

Thanks!

@nalexn
Copy link
Owner

nalexn commented Aug 1, 2024

How is the body property of types like VStack resolved in this case?

This line.

It also uses Inspector.attribute(path: ), but without a cast. Casting happens later, as you attempt to unwrap the child view.

Like, I'm wondering what the difference is between these types?

struct CustomContainer<Content>: View where Content : View {}
struct VStack<Content> : View where Content : View {}

The difference is that VStack is pre-defined in SDK, its structure is fixed and the same for all. It also only references child views.

Custom view can have arbitrary inner structure, reference child views and arbitrary properties. If it's a property outside SwiftUI hierarchy, like Int property from the original question, the way to access it is described in my original answer. You can as well use Inspector.attribute(path: ) for reaching child view, its' child view, etc., but it's error-prone. That's why I suggest you use find because it takes all the complexity away.

@nalexn
Copy link
Owner

nalexn commented Aug 1, 2024

Alternatively you can introduce a protocol without generics, like

protocol MyCustomContainerView: View {
    var contentView: Any { get }
}

conform to that: struct CustomContainer<Content>: MyCustomContainerView, View { ... }, and then use that protocol in inspection chain instead of .view(CustomContainer.self). Haven't tested, lmk if this works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants