简体   繁体   中英

Bug with NavigationLink and @Binding properties causing unintended Interactions in the app

I've encountered a bug in SwiftUI that could cause unintended interaction with the app without the user's knowledge .

Description

The problem seems to be related to using @Binding properties on the View structs when used in conjunction with NavigationStack and NavigationLink . If you use NavigationView with NavigationLink to display a DetailView that accepts a $Binding parameter, and that parameter is used in some sort of condition in the DetailView, it will result in unexpected behavior.

To clearly show the problem, I'm using a DetailView where the "Blue" or "Red" view is shown depending on the @Binding property. Each of those views has a.onTapGesture() modifier that prints some text when tapped. The problem is that if the Red view is shown, it detects and triggers the action on the Blue view, which could lead to unintended changes in many apps without the user's knowledge.

Replication of the problem

You can easily copy and paste this code into your own file to replicate the bug. To see the unexpected behavior, run the code below and follow these steps on the simulator:

  1. Tap on the DetailView in the NavigationLink.
  2. Tap the blue color area and the console will print "Blue Tapped".
  3. Tap the "RED BUTTON" to switch to the other view.
  4. Tap the red color area and the console will print "Red Tapped".
  5. Now try to tap a blank space below the red area (where the blue area was previously located). The console will print "BLUE tapped" - this is the problem, it seems that the blue view is still active there.

I tested this behavior on: XCode 14.1, iPhone 13 Pro 16.1 iOS Simulator, and on a real iPhone with iOS 16. The result was always the same.

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        NavView()
    } 
}

struct NavView: View {
    
    @State private var colourShowed: Int = 1
    
    var body: some View {
        // If the DetailView() was shown directly, (without the NavigationLink and NavigationStack) there would be no such a bug.
        // DetailView(colourShowed: $colourShowed)
        
        // The bug is obvious when using the NavigationStack() with the NavigationLink()
        NavigationStack {
            Form {
                NavigationLink(destination: { DetailView(colourShowed: $colourShowed) },
                               label: { Text("Detail View") })
            }
        }
    }
}

struct DetailView: View {
    
    // It seems like the problem is related to this @Binding property when used in conjunction
    // with the NavigationLink in "NavView" View above.
    @Binding var colourShowed: Int
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20){
                
                HStack {
                    Button("BLUE BUTTON", action: {colourShowed = 1})
                    Spacer()
                    Button("RED BUTTON", action: {colourShowed = 2})
                }
                
                if colourShowed == 1 {
                    Color.blue
                        .frame(height: 500)
                    // the onTapeGesture() is stillActive here even when the "colourShowed" property is set to '2' so this
                    // view should therefore be deinitialized.
                        .onTapGesture {
                            print("BLUE tapped")
                        }
                    // The onAppear() doesn't execute when switching from the Red view to the Blue view.
                    // It seems like the "Blue" View does not deinitialize itself after being previously shown.
                        .onAppear(perform: {print("Blue appeared")})
                }
                else {
                    Color.red
                        .frame(height: 100)
                        .onTapGesture {
                            print("RED tapped")
                        }
                        .onAppear(perform: {print("Red appeared")})
                }
            }
        }
    }
}

Is there any solution to prevent this?

This is a common problem encountered by those new to Swift and value semantics, you can fix it by using something called a "capture list" like this:

NavigationLink(destination: { [colourShowed] in

It occurred because DetailView wasn't re-init with the new value of colourShowed when it changed. Nothing in body was using it so SwiftUI's dependency tracking didn't think body had to be recomputed. But since you rely on DetailView being init with a new value you have to add it to the capture list to force body to be recomputed and init a new DetailView.

Here are other questions about the same problem with .sheet and .task .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM