swiftuienvironmentobjectviewmodifier

SwiftUI: @EnvironmentObject with ViewModifier?


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.


Solution

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