Switches to another View for no reason

I've shortened my code as much as possible. The problem is that when I click on the "Generate" button inside CreateView, I am transferred to WaitView for a tenth of a second, after which I am transferred back to CreateView.

I quickly realized that the problem was a modification of the DataManager, since when I deleted this code: dataManager.data.append(SomeData(data: newData)) this problem disappeared. But this obviously doesn’t suit me, because I need to change the data. I also noticed that if you remove this code from ListView, then everything will work:

NavigationLink(destination: CreateView(dataManager: dataManager)) {
    Text(data_item.data)
}

I can't imagine how this should affect this at all. What could be the problem?

code:

import Foundation
import SwiftUI


struct SomeData: Identifiable {
    let id = UUID()
    var data: String
}

class DataManager: ObservableObject {
    @Published var data: [SomeData]
    init() {
        data = [SomeData(data: "test")]
    }
}

struct ContentView: View {
    @StateObject var dataManager: DataManager = DataManager()
    var body: some View {
        NavigationView {
            ListView(dataManager: dataManager)
            .navigationTitle("First View")
        }
    }
}

struct ListView: View {
    @ObservedObject var dataManager: DataManager
    
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: CreateView(dataManager: dataManager)) {
                    Text("GENERATE")
                        .font(.title)
                        .foregroundColor(.white)
                        .padding()
                        .frame(width: 200, height: 200)
                        .background(Color.blue)
                        .clipShape(Circle())
                    
                }
                .padding(100)
                
                
                Spacer()
                VStack (alignment: .leading) {
                    Text("History")
                        .font(.title3)
                        List {
                            ForEach(dataManager.data) {data_item in
                                NavigationLink(destination: CreateView(dataManager: dataManager)) { //When I remove this line everything works
                                    Text(data_item.data)
                                }
                            }
                            
                        }
                        .listStyle(.automatic)
                        .cornerRadius(20)

                }
                .padding()
            }
        }
    }
}

struct CreateView: View {
    @ObservedObject var dataManager: DataManager
    @State var fieldData: String = "some data here"
    
    var body: some View {
        UITableView.appearance().backgroundColor = .clear
        return VStack {
            NavigationLink(destination: WaitView(dataManager: dataManager, newData: fieldData)) {
                Text("Generate")
                    .padding()
                    .background(Color.red)
            }

        }
        .navigationBarTitleDisplayMode(.inline)
    }}

struct WaitView: View {
    @ObservedObject var dataManager: DataManager
    let newData: String
    
    var body: some View {
        Text("Wait for something...")
            .navigationTitle("Third View")
            .onAppear {
                dataManager.data.append(SomeData(data: newData))//When I remove this line everything works
                print("adding")
            }
    }
}
Answered by Claude31 in 765612022

I solved it by embedding in a NavigationView :

    var body: some View {
        NavigationView {
//        UITableView.appearance().backgroundColor = .clear
            return VStack {
                NavigationLink(destination: WaitView(dataManager: dataManager, newData: fieldData)) {
                    Text("Generate")
                        .padding()
                        .background(Color.red)
                }
            }
        }
        .navigationBarTitleDisplayMode(.inline)
    }

Note: return is useless.

But now, the problem is you get 2 back buttons. I solved it by hidding the first back button when needed.

  • cerate a hideFirstBackButton state Bool
  • pass it to a Binding to CreateView where it will be set to true
struct CreateView: View {
    @ObservedObject var dataManager: DataManager
    @State var fieldData: String = "some data here"
    @State var hideFirstBackButton = false  // We can hide the back button of this NavigationView
 
    var body: some View {
        NavigationView {
//        UITableView.appearance().backgroundColor = .clear
//            return
            VStack {
                NavigationLink(destination: WaitView(dataManager: dataManager, newData: fieldData, hideFirstBackButton: $hideFirstBackButton)) {
                    Text("Generate")
                        .padding()
                        .background(Color.red)
                }
            }
        }
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarBackButtonHidden(hideFirstBackButton)
        .onAppear {
            UITableView.appearance().backgroundColor = .clear
        }
    }

}

Now, you need to restore when you leave WaitView:

struct WaitView: View {
    @Environment(\.isPresented) var isPresented     // To detect we left the view in order to restore back button https://stackoverflow.com/questions/61930915/swiftui-detecting-the-navigationview-back-button-press
    @ObservedObject var dataManager: DataManager
    let newData: String
    @Binding var hideFirstBackButton : Bool
    
    var body: some View {
        Text("Wait for something...")
            .navigationTitle("Third View")
            .onAppear {
                hideFirstBackButton = true
                dataManager.data.append(SomeData(data: newData))//When I remove this line everything works
                print("adding")
            }
            .onChange(of: isPresented) { newValue in
                if !newValue {
                    print("WaitView is dismissed")
                    hideFirstBackButton = false  // We restore the button
                }
            }
    }
}

Works well, but there may be a much simpler solution.

Don't use NavigationLink, but manage directly the appearance of WaitView:

   @State var showWaitView = false

    var body: some View {
        if showWaitView {
            WaitView(dataManager: dataManager, newData: fieldData)
        } else {
            Button(action: {
                self.showWaitView.toggle()
            }) {
                Text("Generate")
                    .padding()
                    .background(Color.red)
            }
        }
    }

You would have to create a back button "manually".

Do I miss something ? I don't understand why you're suprised.

Is this the GENERATE you speak about ? This link (not a button) goes to WaitView

 NavigationLink(destination: WaitView(dataManager: dataManager, newData: fieldData)) {
                Text("Generate")

The code in .onAppear probably delays the transition to the next view for a tenth of a second. If you remove, you have no time to see the intermediate View.

But if you change to

            .onAppear {
                sleep(2)
                print("adding")
            }

You should see the WaitView for 2 seconds.

Is that the case ?

I do not get the sequence.

 

I click on the link in CreateView, it takes me to WaitView, saves the data in the DataManager, and I remain in WaitView.

You told before that it returned (automatically) after a 1/10 second. Isn't it the case ?

 

Whenever I want, I click on the "back" link and it takes me back to CreateView.

So you return when you hit back, not automatically ?

 

But instead it transfers me back, although this is not written down anywhere.

back to which view ?

Accepted Answer

I solved it by embedding in a NavigationView :

    var body: some View {
        NavigationView {
//        UITableView.appearance().backgroundColor = .clear
            return VStack {
                NavigationLink(destination: WaitView(dataManager: dataManager, newData: fieldData)) {
                    Text("Generate")
                        .padding()
                        .background(Color.red)
                }
            }
        }
        .navigationBarTitleDisplayMode(.inline)
    }

Note: return is useless.

But now, the problem is you get 2 back buttons. I solved it by hidding the first back button when needed.

  • cerate a hideFirstBackButton state Bool
  • pass it to a Binding to CreateView where it will be set to true
struct CreateView: View {
    @ObservedObject var dataManager: DataManager
    @State var fieldData: String = "some data here"
    @State var hideFirstBackButton = false  // We can hide the back button of this NavigationView
 
    var body: some View {
        NavigationView {
//        UITableView.appearance().backgroundColor = .clear
//            return
            VStack {
                NavigationLink(destination: WaitView(dataManager: dataManager, newData: fieldData, hideFirstBackButton: $hideFirstBackButton)) {
                    Text("Generate")
                        .padding()
                        .background(Color.red)
                }
            }
        }
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarBackButtonHidden(hideFirstBackButton)
        .onAppear {
            UITableView.appearance().backgroundColor = .clear
        }
    }

}

Now, you need to restore when you leave WaitView:

struct WaitView: View {
    @Environment(\.isPresented) var isPresented     // To detect we left the view in order to restore back button https://stackoverflow.com/questions/61930915/swiftui-detecting-the-navigationview-back-button-press
    @ObservedObject var dataManager: DataManager
    let newData: String
    @Binding var hideFirstBackButton : Bool
    
    var body: some View {
        Text("Wait for something...")
            .navigationTitle("Third View")
            .onAppear {
                hideFirstBackButton = true
                dataManager.data.append(SomeData(data: newData))//When I remove this line everything works
                print("adding")
            }
            .onChange(of: isPresented) { newValue in
                if !newValue {
                    print("WaitView is dismissed")
                    hideFirstBackButton = false  // We restore the button
                }
            }
    }
}

Works well, but there may be a much simpler solution.

Don't use NavigationLink, but manage directly the appearance of WaitView:

   @State var showWaitView = false

    var body: some View {
        if showWaitView {
            WaitView(dataManager: dataManager, newData: fieldData)
        } else {
            Button(action: {
                self.showWaitView.toggle()
            }) {
                Text("Generate")
                    .padding()
                    .background(Color.red)
            }
        }
    }

You would have to create a back button "manually".

Actually, .navigationViewStyle(.stack) is the answer. I don't know why, it's just better than the usual one.

Switches to another View for no reason
 
 
Q