Sheet 删除标签时无法关闭

Sheet can't be dismissed when removing a tab

在我的应用程序中,我有两个选项卡。第二个选项卡根据某些条件显示或隐藏。我发现当要隐藏选项卡时,如果在第二个选项卡中显示 sheet,则无法关闭 sheet。

使用以下代码可以一致地重现该问题。要重现它,请单击选项卡 2,然后单击“Present Sheet”,然后单击“隐藏选项卡 2”。您会看到 sheet 没有被删除,尽管包含它的选项卡(即选项卡 2)已被删除(您可以将 sheet 向下拖动以验证它)。

对我来说这似乎是一个 SwiftUI 错误。有谁知道如何解决它?我即将完成我的应用程序,但遇到了这个意想不到的问题 :( 非常感谢任何帮助。

struct ContentView: View {
    @State var showTab2: Bool = true
    
    var body: some View {
        TabView {
            // tab 1
            NavigationView {
                Text("Tab 1")
            }
            .tabItem {
                Label("Tab 1", systemImage: "1.circle")
            }
            // tab 2
            if showTab2 {
                NavigationView {
                    Tab2(showTab2: $showTab2)
                }
                .tabItem {
                    Label("Tab 2", systemImage: "2.circle")
                }
            }
        }
    }
}

struct Tab2: View {
    @State var showSheet: Bool = false
    @Binding var showTab2: Bool

    var body: some View {
        VStack(spacing: 12) {
            Text("Tab 2")
            Button("Click to present sheet") {
                showSheet = true
            }
        }
        .sheet(isPresented: $showSheet, onDismiss: nil) {
            NavigationView {
                MySheet(showTab2: $showTab2)
            }
        }
    }
}

struct MySheet: View {
    @Environment(\.dismiss) var dismiss
    @Binding var showTab2: Bool

    var body: some View {
        Button("Click to hide tab 2") {
            // dismiss() works fine if I comment out this line.
            showTab2 = false
            dismiss()
        }
    }
}

我已经向 Apple 提交了对此的反馈,但我对任何答复都不乐观(我从未收到过)。

更新:

此问题可以在许多其他不涉及 sheet 的情况下重现。所以,@Asperi 给出的第二种方法不是通用的解决方案。

好吧,这里我们看到了动作冲突(由于赛车):异步 sheet 关闭(由于动画)和同步选项卡删除。

以下是可能的方法:

  1. 延迟选项卡在 sheet 关闭后删除(隐式方式)
Button("Click to hide tab 2") {
    dismiss()
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {  // << here !!
        showTab2 = false
    }
}
  1. 在 sheet 关闭后删除选项卡(显式方式)
.sheet(isPresented: $showSheet, onDismiss: { showTab2 = false }) { // << here !!
    NavigationView {
        MySheet(showTab2: $showTab2)
    }
}

注意:实际上当视图 knows/manages 父级的父级的东西不是很好的设计,所以选项 2(可能有一些额外的 conditions/callbacks)更可取。

@Asperi 给出了很好的答案。但将他的方法应用到实际应用程序中并不简单。我将在下面解释为什么以及如何做到这一点。

Asperi 方法的关键思想是,由于 UI 更改具有竞争条件,因此应分两步执行。在这两种方法中,sheet 首先被关闭,然后选项卡被隐藏。

然而,在实践中,如何分离这两个步骤可能并不明显。例如,我的应用程序是这样工作的(我认为这是典型的):

  • sheet 包含一个表单并调用数据模型 API 以在用户提交表单时改变数据模型。
  • 由于数据模型 API 可能会失败,因此 sheet 不会在用户提交表单后立即自行关闭。相反,它仅在 API 调用成功时执行此操作(API 调用是同步的)。
  • 当数据模型发生变异时,可能会触发隐藏选项卡的条件。

注意第 2 项和第 3 项。这意味着 sheet 必须先调用数据模型 API,这可能会隐藏选项卡,然后自行关闭。

我花了一段时间才想出解决方案 - 引入一个专用状态来控制 show/hide 选项卡,从而分离这两个步骤。现在剩下的问题是如何将数据模型更改同步到该状态。由于目的是使它们显示为对 UI 的两个单独更改,因此我们不能使用 Combine。如果不实现它可能会很混乱 属性 因为数据模型可以从任何地方发生变化(例如 Form、ActionSheet 或只是 Button)。幸运的是我找到了一个非常优雅的方法:

.onChange(of: model.showTab2) { value in
    // In my experiments async() works fine, but just to be on the safe side...
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        // This is a state outside data model. It hides/shows tab2.
        showTab2 = value
    }   
}

这又是一个例子,没有加一层抽象解决不了的问题:)