1

Here is the idea of what I would like to do: my app's navigation contains a list, which displays a view on tap gesture which is triggered with a ViewModifier to perform an animation.

In a nutshell:

MyList > MyViewModifier > MyView

Since I want to pass some data from the list to the final view in this hierarchy, I was thinking of using @StateObject/@EnvironmentObject property wrappers. But I just realized I can't pass an .environmentObject(xx) to a ViewModifier like I would do with a View.

Here is the code I have so far:

struct TestModel: Identifiable {
    var id: Int
    var name: String
    //@Published var whateverData I would like to fetch later and see appear in other views?
}

class TestObject: ObservableObject {
    @Published var elements: [TestModel] = [
        TestModel(id: 1, name: "Element 1"),
        TestModel(id: 2, name: "Element 2"),
        TestModel(id: 3, name: "Element 3")
    ]
}

struct MyList: View {
    @StateObject var testObject = TestObject()
    @State var presenting = false
    @State var tappedId: Int?
    
    var body: some View {
        List(testObject.elements) { element in
            Text(element.name)
                .onTapGesture {
                    tappedId = element.id
                    presenting = true
                }
        }
        .modifier(
            MyViewModifier(presented: $presenting, tappedId: tappedId)
            // .environmentObject(testObject) ?
        )
    }
}

struct MyViewModifier: ViewModifier {
    @Binding var presented: Bool
    var tappedId: Int?
    
    @EnvironmentObject var testObject: TestObject
    
    func body(content: Content) -> some View {
        content.overlay(
            ZStack {
                if presented {
                    MyView(testObject: _testObject, tappedId: tappedId)
                }
            }
        )
    }
}

struct MyView: View {
    @EnvironmentObject var testObject: TestObject
    var tappedId: Int?
    
    var body: some View {
        ZStack {
            Color.white
            Text(testObject.elements.first(where: { $0.id == tappedId })?.name ?? "-")
        }
        .frame(width: 250, height: 250)
    }
}

Of course this throws an error when trying to display TestView:

No ObservableObject of type TestObject found. A View.environmentObject(_:) for TestObject may be missing as an ancestor of this view.

(FYI the reason why I want to use an environment object and pass it to the modifier and then the view is that I may want to call an API in TestView when it appears on screen, which would fetch more information of this object and dispatch this information both in MyList and MyView)

What are your recommendations here? Thank you for your help.

1
  • Why not just passing testObject as a parameter to the modifier? Ex. MyViewModifier(presented: $presenting, tappedId: tappedId, test: testObject) Commented May 20, 2022 at 10:04

1 Answer 1

3

As it was suggested in the comments, you could pass the object as a parameter to your ViewModifier.

Another option is to call .environmentObject() after .modifier() and you should be able to read that object from your modifier.

struct MyList: View {
    @StateObject var testObject = TestObject()
    @State var presenting = false
    @State var tappedId: Int?
    
    var body: some View {
        List(testObject.elements) { element in
            // blah blah
        }
        .modifier(MyViewModifier(presented: $presenting, tappedId: tappedId))
        .environmentObject(testObject)
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

I just got it to work this way. Thank you! I need to learn a little better how EnvironmentObjects are being passed from components to components now.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.