0

I'm trying to pass an EnvironmentObject to child views but am having no luck with the following code.

struct ContentView: View {
  enum MyViews: Int, CaseIterable {

    case introView
    case viewOne
    case viewTwo

    var activeView: AnyView {
      switch self.rawValue {
        case 0: return AnyView(IntroView())
        case 1: return AnyView(ViewOne())
        case 2: return AnyView(ViewTwo())
        default: return AnyView(IntroView())
      }
    }

    @State var pageIndex = 0

    func content() -> MyViews? {
        let newPage = MyViews.init(rawValue: pageIndex)
        return newPage
    }
}

var body: some View {
   Group {
    content()?.activeView // Problem appears to lie here
   }
   .environmentObject(userData)
}

If I replace content()?.activeView with a simple view e.g. TestView() then I'm able to successfully print out the userData variable in TestView(). But as it stands, I get a crash when trying to access userData in ViewOne(), even when ViewOne is identical to TestView().

Any idea what I'm doing wrong?

Ribena
  • 1,086
  • 1
  • 11
  • 20
  • 1
    SwiftUI injects environment when see view building in body, when you return view from function it does see view construction, so either do it manually or, better, don't do that at all - you disable in such case many SwiftUI features and optimisations. – Asperi Dec 12 '20 at 14:31
  • Can't really reproduce. I can access EnvironmentObject inside Child Views – davidev Dec 12 '20 at 14:33
  • @Asperi I think you're right and that I probably shouldn't do it this way. But if I really did want to, how would I get around this error? – Ribena Dec 12 '20 at 14:55
  • @davidev I've just recreated a minimal project with this and it's definitely giving me a fatal error. – Ribena Dec 12 '20 at 14:56
  • Can you share your minimal project please – davidev Dec 12 '20 at 15:07
  • @davidev Sure thing ... https://github.com/transat/Test – Ribena Dec 12 '20 at 15:15
  • @davidev I've pushed a new version of the minimal project and this time it breaks. – Ribena Dec 12 '20 at 16:17
  • Thank you.. I edited my answer – davidev Dec 12 '20 at 19:13

1 Answers1

1

The problem is that you need to pass your EnvironmentObject manually via init() to your viewModel. It won't be injected automatically. Here is a approach how to do it

func getActiveView(userData: UserData) -> AnyView {
    switch self.rawValue {
    case 0: return AnyView(IntroView(userData: userData))
      case 1: return AnyView(ViewOne())
    default: return AnyView(IntroView(userData: userData))
    }
}

In your View of ContentView call the function and pass the userData

ZStack {
    content()?
        .getActiveView(userData: userData)
        .environmentObject(userData)
}

IntroView and ViewModel take userData as parameter

class AListViewModel: ObservableObject {
    
    var userData: UserData
    init(userData: UserData) {
        self.userData = userData
    }

    func myFunc(){
        print("myVar: \(userData.myVar)")
    }
}

struct IntroView: View {
    @EnvironmentObject var userData: UserData
    
    @ObservedObject var someListVM : AListViewModel
    
    init(userData: UserData) {
        someListVM = AListViewModel(userData: userData)
    }
davidev
  • 7,694
  • 5
  • 21
  • 56
  • You're right, I did forget this from the Test app and the minimal reproduction does work now. However in my actual app I do have this line in there and it's not working. I think @Asperi might be right in that I'm probably not following best practice, in any case. I was basically trying to do this: https://github.com/exyte/LiquidSwipe but they just change the colour of a Rectangle with the pageIndex variable and I thought that I could use a bunch of different views instead. It was working fine until this EnvironmentObject issue. – Ribena Dec 12 '20 at 15:26
  • 1
    You can not use @EnvironmentObject inside your ViewModel. Hence you have to pass that object manually to the viewModel. Here is a solution. Working fine for me – davidev Dec 12 '20 at 19:14