3

Currently in my project, I have a navigation stack where at the parent level NavigationView, there's Timer that kicks off a withAnimation block that animates a property that buttons within the navigation stack share.

After the update to iOS 14.5, when a NavigationLink is hit in the middle of a withAnimation block (which is always in this case), the EnvironmentObjects that are passed down the navigation stack become nil.

Error thrown: Fatal error: No ObservableObject of type BackgroundViewModel found. A View.environmentObject(_:) for BackgroundViewModel may be missing as an ancestor of this view.

Definitely think it is a combination of the new way NavigationView handles animations and the way NavigationLinks handle state. Per iOS 14.5 developer release notes: NavigationView push and pop now correctly respects disabled animations. (70062477) & The destination of NavigationLink that only differs by local state now resets that state when switching between links as expected. (72117345)

Below is a sample that will recreate the error. Any help would be greatly appreciated.

import SwiftUI
import Combine

struct ContentView: View {
    @StateObject var backgroundVM = BackgroundViewModel()
    @State var presentNext: Bool = false
    
    var body: some View {
        NavigationView {
            VStack{
                Text("Hello, world!")
                    .padding()
                NavigationLink(
                    destination: View2(),
                    isActive: $presentNext) {
                    Button {
                        presentNext = true
                    } label: {
                        Text("Navigate")
                    }
                }
            }
            .frame(width: 250, height: 250, alignment: .center)
            .background(backgroundVM.backgroundColor)
        }
        .environmentObject(backgroundVM)
        .onReceive(backgroundVM.timer) { _ in
            withAnimation(.easeInOut(duration: 2.0)) {
                backgroundVM.setColor()
            }
        }
    }
}

struct View2: View {
    @EnvironmentObject var backgroundVM: BackgroundViewModel
   
    @State var presentNext: Bool = false
    
    var body: some View {
        VStack {
            Text("View 2")
            NavigationLink(
                destination: View3(),
                isActive: $presentNext) {
                Button {
                    presentNext = true
                    
                } label: {
                    Text("Navigate")
                }
            }
        }
        .frame(width: 250, height: 250, alignment: .center)
        .background(backgroundVM.backgroundColor)
    }
}

struct View3: View {
    @EnvironmentObject var backgroundVM: BackgroundViewModel
    
    var body: some View {
        VStack {
            Text("View 3")
        }
        .frame(width: 250, height: 250, alignment: .center)
        .background(backgroundVM.backgroundColor)
 
    }
}

class BackgroundViewModel: ObservableObject {
    @Published var backgroundColor: Color = .orange
    @Published var colorIndex: Int = 0 {
        willSet {
            backgroundColor = colors[newValue]
        }
    }
    
    var timer = Timer.publish(every: 1.5, on: .main, in: .common)
        .autoconnect()
    
    var colors: [Color] = [.orange, .green, .purple]
    
    func setColor() {
        if colorIndex + 1 == colors.count {
            colorIndex = 0
        } else {
            colorIndex += 1
        }
    }
}
NFarrell
  • 255
  • 1
  • 17

2 Answers2

0

Try

View2().environmentObject(backgroundVM)

And

 View3().environmentObject(backgroundVM)

You have to use this

 .environmentObject(backgroundVM)

Every time you instantiate a view that has

 @EnvironmentObject
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • 1
    Thanks for the response lorem! Yea I unfortunately I am using this as a workaround but doesn't doing this defeat the purpose of an EnvironmentObject? Why not just add those objects as parameters for instantiating those views? iOS 14.4 let me pass the EnvironmentObjects all the way down the stack using the ```.environmentObject(object)``` call at the parent NavigationView level. – NFarrell May 10 '21 at 01:21
  • You aren’t calling it at all in View2 that was your biggest issue. You can add them as parameters if you use the ObservedObject wrapper. I’ve never called it from the bottom and if Apple changed it it is likely to correct a bug. Apple is clear in the documentation that it is supposed to be attached to the View that will use it. – lorem ipsum May 10 '21 at 09:44
  • Gotcha. If you comment the ```withAnimation``` block, you don't have to pass the EnvironmentObject to each view. That's what makes me think there is something wrong – NFarrell May 10 '21 at 14:29
0

I'm having the same issue. You can check out this solution with a singleton from the following answer in the meanwhile:

EnvironmentObject vs Singleton in SwiftUI?

This is happening on iOS 14.6 (just released) too, as well as a bug in navigation on SwiftUI. So be careful about that:

SwiftUI NavigationLink for iOS 14.5 not working

  • Thanks for the response Angel. My issue here is that, since the app is always animating, the `NavigationLink` would nil my `EnvironmentObjects` on segue. Did you have a similar situation that you were able to solve using the singleton solution? – NFarrell May 27 '21 at 15:48
  • 1
    I had 3 environment objects being passed trough my app file to the root view. Singleton solution made it except for the one I was using for push notifications. Any errors but notifications were not handled or displayed. I was just using `.withAnimation` blocks for show/hide the loader and for the view router for changing the presented main view. I ended up by returning the notifications object to `@EnvironmentObject`, and fixing the bug adding the `.environmentObject` call **only to the destination view of the navigation link where the object was getting nil**. – Angel Chavez Reyes May 28 '21 at 16:14
  • Solution implemented for push notifications that did not work as singleton instead of `@EnvironmentObject` can be found [here](https://prafullkumar77.medium.com/how-to-handle-push-notifications-in-swiftuis-new-app-lifecycle-7532c21d32d7) – Angel Chavez Reyes May 28 '21 at 16:16
  • 1
    Okay great I did that same thing to fix this bug as well. That be passing the `@EnvironmentObject` to the destination view where the object was becoming nil. Seems a bit weird that `NavigationLink`s aren't able to handle that. Hopefully Apple fixes it soon. Worked for iOS 14.4 – NFarrell Jun 02 '21 at 19:42