xcodeswiftuixcode11.4

How to create Generic if @EnvironmentObject?


I've recently come across the need to write a Mock of a Class, as it causes the SwiftUI preview from working. Unfortunately, I get the error:

Property type 'T' does not match that of the 'wrappedValue' property of its wrapper type 'EnvironmentObject'

In the View struct:

struct ContentView<T>: View {
    @EnvironmentObject var mockFoobar: T
    ...
}

And also the error:

Type of expression is ambiguous without more context

For the Preview struct:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let mockFoobar: MockFoobar = MockFoobar()
        return ContentView<MockFoobar>()
            .environmentObject(mockFoobar)
    }
}

The MockFoobar class is:

class MockFoobar: ObservableObject {
  ...
}

As the user @Asperi kindly provided, tested the following as suggested:

class Foobar: ObservableObject {
    @Published var param: Bool = false
    func start() {
        self.param = true
    }
}

struct MyFoobarView<T: ObservableObject>: View {
    @EnvironmentObject var foobar: T
    
    var body: some View {
        VStack {
            Text("Hello Foobar")
        }
        .onAppear {
            self.foobar.start()
        }
    }
}

struct MyFoobarView_Previews: PreviewProvider {
    static var previews: some View {
        let foobar: Foobar = Foobar()
        return MyFoobarView()
            .environmentObject(foobar)
    }
}

But I get the following errors (the first in the .onAppear and the second in the PreviewProvider):

Cannot call value of non-function type 'Binding<Subject>'

Generic parameter 'T' could not be inferred

Solution

  • The EnvironmentObject must be ObservableObject, so here is fix

    struct ContentView<T: ObservableObject>: View {
        @EnvironmentObject var mockFoobar: T
        
        // .. other code here
    

    Update: added demo with introduced model protocol

    protocol Foobaring {
        var param: Bool { get set }
        func start()
    }
    
    class Foobar: ObservableObject, Foobaring {
        @Published var param: Bool = false
        func start() {
            self.param = true
        }
    }
    
    struct MyFoobarView<T: ObservableObject & Foobaring>: View {
        @EnvironmentObject var foobar: T
    
        var body: some View {
            VStack {
                Text("Hello Foobar")
            }
            .onAppear {
                self.foobar.start()
            }
        }
    }
    
    struct MyFoobarView_Previews: PreviewProvider {
        static var previews: some View {
            let foobar: Foobar = Foobar()
            return MyFoobarView<Foobar>()
                .environmentObject(foobar)
        }
    }