0

Don't know if I'm abusing the idea of environment object, but experiencing an issue when using an environment object that publishes a delayed async value. One view navigates to the next, but then the 'root' gets updated subsequently and as a result causes an 'echo', or even if that is handled a navigation problem. The issue becomes even more evident when using transitions between navigation. Is there a correct use pattern to avoid this? Or some other solution maybe?

Any guidance will be appreciated.

Attached a condensed sample to illustrate the problem.

Xcode 12.4 ios 14.1

final class SetColor: ObservableObject {
    @Published var asyncVal: Bool = false
    
    func flipIt() {
        DispatchQueue.main.asyncAfter(deadline: .now()+0.5, execute: {self.asyncVal.toggle()})
    }
}

struct HomeView: View {
    @StateObject var setCol: SetColor = SetColor()
    @State private var navActive: Bool = false
    var body: some View {
        NavigationView {
            ZStack {
                Color(setCol.asyncVal ? .blue : .purple)
                Button(action: {
                    setCol.flipIt()
                    navActive.toggle()
                }, label: {
                    Text("Change and Move")
                })
                .navigationTitle("Home")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        NavigationLink(destination: NavChild1().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 1 >") })
                    }
                }
            }
        }
    }
}

struct NavChild1: View {
    @EnvironmentObject var setCol: SetColor
    @State private var navActive: Bool = false
    var body: some View {
        ZStack {
            Color(setCol.asyncVal ? .yellow : .orange)
            Button(action: {
                setCol.flipIt()
                navActive.toggle()
            }, label: {
                Text("Change and Move")
            })
            .navigationTitle("Nav 1")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink(destination: NavChild2().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 2 >") })
                }
            }
        }
    }
}

struct NavChild2: View {
    @EnvironmentObject var setCol: SetColor
    @State private var navActive: Bool = false
    var body: some View {
        ZStack {
            Color(setCol.asyncVal ? .yellow : .orange)
            Button(action: {
                setCol.flipIt()
                navActive.toggle()
            }, label: {
                Text("Change and Move")
            })
            .navigationTitle("Nav 2")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink(destination: NavChild3().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 3 >") })
                }
            }
        }
    }
}

struct NavChild3: View {
    @EnvironmentObject var setCol: SetColor
    @State private var navActive: Bool = false
    var body: some View {
        ZStack {
            Color(setCol.asyncVal ? .yellow : .orange)
            Button(action: {
                setCol.flipIt()
                navActive.toggle()
            }, label: {
                Text("Change and Move")
            })
            .navigationTitle("Nav 3")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink(destination: NavChild3().environmentObject(setCol), isActive: .constant(false), label: { Text("Go Home") })
                }
            }
        }
    }
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView()
    }
}

1 Answers1

0

You do not need the deadline you put in GCD action. It causes navigation actions even if user does not press on navigation (I've tested the code in a project). This is because you accumulate jobs in the GCD queue and when they are executed, you're in another View (due to the 0.5 stall). By the way, they cause navigation since the flip is Observed and therefore whoever listens , will execute the navigation.

Anyway, what you wanna do is change the dispatch command to this:

DispatchQueue.main.async { self.asyncVal.toggle() }

And navigation will be smoother with no extra navigation commands executed afterwards.

Ethan Halprin
  • 470
  • 3
  • 11
  • The deadline is just for this example. (The actual event is a data fetch operation, but don't want to bore somebody with that.) But it does have a delay, not quite that long, but nevertheless a significant enough time to cause an observable 'duplication'. In this case I suppose I can wait for a return value, or alert on timeout, and it wouldn't be much of an issue, but it's the method I'm concerned about. Thinking along the lines of ensuring that the view only updates when it's top. (Originally had Observed and not the current binding.) – Mark Robberts Apr 27 '21 at 22:39