简体   繁体   English

NavigationLink 和 @Binding 属性的错误导致应用程序中出现意外交互

[英]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 .我在 SwiftUI 中遇到了一个错误,它可能会导致在用户不知情的情况下与应用程序进行意外交互

Description描述

The problem seems to be related to using @Binding properties on the View structs when used in conjunction with NavigationStack and NavigationLink .该问题似乎与在与 NavigationStack 和 NavigationLink 结合使用时在View 结构上使用@Binding 属性有关。 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.如果您将 NavigationView 与 NavigationLink 一起使用来显示接受 $Binding 参数的 DetailView,并且该参数在 DetailView 中的某种条件下使用,则会导致意外行为。

To clearly show the problem, I'm using a DetailView where the "Blue" or "Red" view is shown depending on the @Binding property.为了清楚地显示问题,我使用了 DetailView,其中根据 @Binding 属性显示“蓝色”或“红色”视图。 Each of those views has a.onTapGesture() modifier that prints some text when tapped.这些视图中的每一个都有一个.onTapGesture() 修饰符,可以在点击时打印一些文本。 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.点击 NavigationLink 中的 DetailView。
  2. Tap the blue color area and the console will print "Blue Tapped".点击蓝色区域,控制台将打印“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".点击红色区域,控制台将打印“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.控制台将打印“BLUE tapped”——这就是问题所在,蓝色视图似乎仍然处于活动状态。

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.我在以下设备上测试了此行为:XCode 14.1、iPhone 13 Pro 16.1 iOS 模拟器,以及带有 iOS 16 的真实 iPhone。结果始终相同。

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:这是 Swift 和值语义的新手遇到的常见问题,您可以使用称为“捕获列表”的东西来解决它,如下所示:

NavigationLink(destination: { [colourShowed] in

It occurred because DetailView wasn't re-init with the new value of colourShowed when it changed.发生这种情况是因为 DetailView 在更改时没有使用colourShowed的新值重新初始化。 Nothing in body was using it so SwiftUI's dependency tracking didn't think body had to be recomputed. body 中没有任何内容使用它,因此 SwiftUI 的依赖项跟踪并不认为 body 必须重新计算。 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.但是由于您依赖于使用新值初始化 DetailView,因此您必须将其添加到捕获列表以强制重新计算 body 并初始化一个新的 DetailView。

Here are other questions about the same problem with .sheet and .task .以下是关于.sheet.task相同问题的其他问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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