3

Navigate to INCR: 3 and tap either the navigation bar back button or the dismiss button and you'll notice that same view is called again but this time it's a new version because the onAppear firstLoad = true and rand is a different value.

If you comment out @Environment(\.dismiss) var dismiss and dismiss() everything works as expected as it did in iOS 14. This issue also occurs with @Environment(\.presentationMode) var presentationMode

Not sure if this is a bug or if I'm making a silly mistake, but this issue is causing a ton of problems for my app because I have to be able to programmatically dismiss a view, so any input would be appreciated.

struct DetailView: View {
    
    @Environment(\.dismiss) var dismiss
    
    @State var isPresenting = false
    
    @State var incrInt = 0
    
    @State var firstLoad = true
    
    @State var rand = Int.random(in: 1..<500)
    
    var body: some View {
        
        Text("INCR: \(incrInt) RAND: \(rand)")
        
        Button("NAVIGATE"){
            isPresenting = true
        }
        Button("DISMISS"){
           dismiss()
        }
        
        .onAppear(perform: {
            
            if firstLoad{
                print("ON APPEAR FIRST LOAD")
                print(incrInt)
                print(rand)
                print("\n")
                firstLoad = false
            }
        })
        
        NavigationLink(destination: DetailView(incrInt: (incrInt + 1)), isActive: $isPresenting){}
        
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView{
            VStack{
                DetailView()
            }
        }
    }
}

Video Link

https://i.imgur.com/qpu7NT7.mp4

Update 1: ViewModel Source of Truth

class DetailViewModel: ObservableObject {
    
    @Published var isPresenting = false
    
    var incr: Int
    
    var rand = Int.random(in: 1..<500)
    
    init(incr: Int){
        
        self.incr = incr
        
        print("INIT FIRST LOAD")
        print(incr)
        print(rand)
        print("\n")
    }
    
}

struct DetailView: View {
    
    @Environment(\.dismiss) var dismiss
    
    @StateObject var detailViewModel: DetailViewModel
    
    var body: some View {
        
        Text("INCR: \(detailViewModel.incr) RAND: \(detailViewModel.rand)")
        
        
        Button("NAVIGATE"){
            detailViewModel.isPresenting = true
        }
        Button("DISMISS"){
            dismiss()
        }
        
        
        NavigationLink(destination: DetailView(detailViewModel: DetailViewModel(incr: (detailViewModel.incr + 1))), isActive: $detailViewModel.isPresenting){}
         
        
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView{
            VStack{
                DetailView(detailViewModel: DetailViewModel(incr: 0))
            }
        }
    }
}

  • I can't reproduce the described behavior on either the simulator or a device. I made a video, and reviewed it carefully. Everything is the same in the stack. Your minimal reproducible example is not capturing the bug. – Yrb Sep 22 '21 at 20:43
  • @Yrb thank you for your response I added a video link showing the issue. As I tap dismiss from each view the view is reloaded with different values. Also a difference might be that I made my iOS Deployment Target iOS 15.0. – K E N N E R Sep 22 '21 at 20:57
  • I am using iOS15 to test, and the project is also set to iOS 15 (I keep it around for SO to test out issues, 1 for 14 and 1 for 15). However, I was looking at the views being shown, not the data being printed to the console. They are different from each other, but the views are still the same that were pushed. Are you using `.onAppear` to do some work in the view when the views are popped? – Yrb Sep 22 '21 at 21:10
  • @Yrb yes, I make a network request in `.onAppear` so each time a view is popped it makes a duplicate network request which is not ideal. – K E N N E R Sep 22 '21 at 21:13
  • You are going to want to rethink that. `.onAppear()` is notoriously unreliable. You should determine whether you need to retrieve or refresh data from your single source of truth. – Yrb Sep 22 '21 at 21:21
  • @Yrb Yeah you're right I'm probably going to start making network requests from the `init()` of my ViewModels – K E N N E R Sep 22 '21 at 21:57
  • I would set it up as a separate function. You can then call it from the `init()` or from somewhere else that checks whether your data is still valid. I have no idea how time sensitive the data is, but at some point it has to expire. – Yrb Sep 22 '21 at 22:00
  • @Yrb This is most definitely a bug because I altered it with a single source of truth and the views are still being reloaded with new values. I added an update with the code to reproduce. – K E N N E R Sep 23 '21 at 00:41

2 Answers2

5

I solved it by adding .navigationViewStyle(.stack) to the NavigationView. I thought that that was the default navigation view style on iOS, but maybe that changed in iOS 15.

  • So my answer did not address your issue adequately? Can you explain what you have solved with adding `.navigationViewStyle(.stack)` to your code. – workingdog support Ukraine Sep 23 '21 at 13:28
  • Thank you for your response but each `DetailView` needs to have it's own source of truth `DetailViewModel` because each view is unique hence the `incr` should be unique in each view. Before I stumbled on `.navigationViewStyle(.stack)` each view that was dismissed was causing the `DetailViewModel` in that view to init in the background again which is not ideal because I call a function that makes a network request in the `init()` on first load. – K E N N E R Sep 23 '21 at 13:37
  • what about in iOS 14? – Tanvirgeek Nov 01 '21 at 10:12
2

In your "Update" code, you are not using a single source of truth. You are creating and passing a new DetailViewModel into DetailView every time you click on the NavigationLink. Use only 1 DetailViewModel, and pass it around. In addition, you are changing isPresenting, so all your views that rely on this will be updated with the "new" value. This cascading is not what you want. Modify your logic. Using DetailViewModel is a good idea to keep the state of your model across views. Try something like this:

class DetailViewModel: ObservableObject {
   // @Published var isPresenting = false  // <-- not relevant
    
    var incr: Int
    var rand = Int.random(in: 1..<500)
    
    init(incr: Int) {
        self.incr = incr
        print("----> DetailViewModel init --> inc: \(incr) --> rand: \(rand) \n")
    }
    
    func doIncr(_ incr: Int) {
        self.incr = incr
        print("----> DetailViewModel doIncr --> inc: \(incr) --> rand: \(rand) \n")
    }
}

struct DetailView: View {
    @Environment(\.dismiss) var dismiss
    @ObservedObject var detailViewModel: DetailViewModel
    @State var showThyself = false  // <--- here
    
    var body: some View {
        Text("DetailView  INCR: \(detailViewModel.incr) RAND: \(detailViewModel.rand)")
        Button("NAVIGATE"){
            detailViewModel.doIncr(detailViewModel.incr + 1)
            showThyself = true
        }
        Button("DISMISS"){
            dismiss()
        }
        NavigationLink(destination: DetailView(detailViewModel: detailViewModel), isActive: $showThyself){}
        .onAppear {
            // do something with the current state of your DetailViewModel
            print("----> DetailView onAppear \n")
        }
    }
}

struct ContentView: View {
    var detailViewModel = DetailViewModel(incr: 0)  // <--- here
    var body: some View {
        NavigationView{
            VStack{
                DetailView(detailViewModel: detailViewModel)
            }
        }
    }
}  
  • Thank you for your help this issue seems to have stopped. The only issue still is that each DetailView in my app is a product which needs a single unique source of truth for each view. Say `DetailView (productID 23)` then the user taps a product on that view and navigates to `DetailView (productID 45)`. How would you go about implementing something like that? – K E N N E R Sep 23 '21 at 12:40