如果未显示所有 NavigationLink,则 NavigationView 中的 SwiftUI 的 NavigationLink `tag` 和 `selection` 停止工作

SwiftUI's NavigationLink `tag` and `selection` in NavigationView stops working if not all NavigationLinks are displayed

我在 NavigationView 中有一个 Form 中的项目列表,每个项目都有一个可以通过 NavigationLink 访问的详细视图。 当我向列表中添加一个新元素时,我想显示它的详细视图。为此,我使用 NavigationLink 作为 selection 接收的 @State var currentSelection,并且每个元素都具有作为 tag:

的功能
NavigationLink(
  destination: DetailView(entry: entry),
  tag: entry,
  selection: $currentSelection,
  label: { Text("The number \(entry)") })

这行得通,并且遵循 Apple docs and the best practises

令人惊讶的是,当列表中的元素多于屏幕上显示的元素(加上 ~2)时,它 停止 工作。问题:为什么?我该如何解决?


我做了一个最小的例子来复制这个行为:

import SwiftUI

struct ContentView: View {
    @State var entries = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
    @State var currentSelection: Int? = nil

    var body: some View {
        NavigationView {
            Form {
                ForEach(entries.sorted(), id: \.self) { entry in
                    NavigationLink(
                        destination: DetailView(entry: entry),
                        tag: entry,
                        selection: $currentSelection,
                        label: { Text("The number \(entry)") })
                }
            }
            .toolbar {
                ToolbarItem(placement: ToolbarItemPlacement.navigationBarLeading) { Button("Add low") {
                    let newEntry = (entries.min() ?? 1) - 1
                    entries.insert(newEntry, at: 1)
                    currentSelection = newEntry
                } }
                ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) { Button("Add high") {
                    let newEntry = (entries.max() ?? 50) + 1
                    entries.append(newEntry)
                    currentSelection = newEntry
                } }
                ToolbarItem(placement: ToolbarItemPlacement.bottomBar) {
                    Text("The current selection is \(String(describing: currentSelection))")
                }
            }
        }
    }
}

struct DetailView: View {
    let entry: Int
    var body: some View {
        Text("It's a \(entry)!")
    }
}

(我通过将列表减少到5项并在标签上设置padding来排除个元素是核心问题:label: { Text("The number \(entry).padding(30)") }))

正如您在屏幕录像中看到的那样,在达到元素的临界数量后(通过添加到列表中或添加到列表中),底部的 sheet 仍然显示 currentSelection 是正在更新,但没有导航发生。

我使用了 iOS 14.7.1、Xcode 12.5.1 和 Swift 5.

问题出在编程导航(由工具栏中的按钮启动) 您的 NavigationLink 需要存在才能被触发。

但是如果使用 List(或 Form,或 LazyVStack),则不应该“创建”不应该出现在屏幕上的子视图(此处:NavigationLinks) ”。因此,当您尝试触发 NavigationLink(修改 currentSelection)时,此 NavigationLink 可能不存在

如果我们用 ScrollView 替换 List / Form,问题应该会消失。

但最好的方法当然是将两种导航方式分开:

1- 程序导航,由按钮触发。为此,我们将使用 NavigationLink(_:destination:isActive:) 初始化器。

2- 通过列表导航(可点击的单元格,又名 NavigationLink(destination:label:

struct ContentView: View {
    @State var entries = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
    @State var currentSelection: Int? = nil
    @State var linkIsActive = false
    var body: some View {
        NavigationView {
            Form {
                ForEach(entries.sorted(), id: \.self) { entry in
                    // 2- Clickable label
                    NavigationLink(
                        destination: DetailView(entry: entry),
                        label: { Text("The number \(entry)") })
                }
            }
            .background(
                // 1- Programmatic navigation
                NavigationLink("", destination: DetailView(entry: currentSelection ?? -99), isActive: $linkIsActive)
            )
            .onChange(of: currentSelection, perform: { value in
                if value != nil {
                    linkIsActive = true
                }
            })
            .onChange(of: linkIsActive, perform: { value in
                if !value {
                    currentSelection = nil
                }
            })
   //...
}

发生这种情况是因为未呈现较低的项目,因此在层次结构中没有 NavigationLink 带有此类标签

我建议你使用 ZStack + EmptyView NavigationLink “hack”。

此外,我在这里使用 LazyView,感谢 @autoclosure,它让我可以通过向上包装 currentSelection:这只会在 NavigationLink 处于活动状态时调用,并且发生在currentSelection != nil

struct ContentView: View {
    @State var entries = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
    @State var currentSelection: Int? = nil

    var body: some View {
        NavigationView {
            ZStack {
                EmptyNavigationLink(
                    destination: { DetailView(entry: [=10=]) },
                    selection: $currentSelection
                )
                Form {
                    ForEach(entries.sorted(), id: \.self) { entry in
                        NavigationLink(
                            destination: DetailView(entry: entry),
                            label: { Text("The number \(entry)") })
                    }
                }
                .toolbar {
                    ToolbarItem(placement: ToolbarItemPlacement.navigationBarLeading) { Button("Add low") {
                        let newEntry = (entries.min() ?? 1) - 1
                        entries.insert(newEntry, at: 1)
                        currentSelection = newEntry
                    } }
                    ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) { Button("Add high") {
                        let newEntry = (entries.max() ?? 50) + 1
                        entries.append(newEntry)
                        currentSelection = newEntry
                    } }
                    ToolbarItem(placement: ToolbarItemPlacement.bottomBar) {
                        Text("The current selection is \(String(describing: currentSelection))")
                    }
                }
            }
        }
    }
}

struct DetailView: View {
    let entry: Int
    var body: some View {
        Text("It's a \(entry)!")
    }
}

public struct LazyView<Content: View>: View {
    private let build: () -> Content
    public init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    public var body: Content {
        build()
    }
}

struct EmptyNavigationLink<Destination: View>: View {
    let lazyDestination: LazyView<Destination>
    let isActive: Binding<Bool>
    
    init<T>(
        @ViewBuilder destination: @escaping (T) -> Destination,
        selection: Binding<T?>
    )  {
        lazyDestination = LazyView(destination(selection.wrappedValue!))
        isActive = .init(
            get: { selection.wrappedValue != nil },
            set: { isActive in
                if !isActive {
                    selection.wrappedValue = nil
                }
            }
        )
    }
    
    var body: some View {
        NavigationLink(
            destination: lazyDestination,
            isActive: isActive,
            label: { EmptyView() }
        )
    }
}

查看有关 LazyView 的更多信息,它通常对 NavigationLink 有帮助:在实际应用程序中,目标可能是一个巨大的屏幕,当每个单元格中都有一个 NavigationLink 时,SwiftUI 将处理所有可能导致滞后的问题

因为可能需要查看表单中添加了哪一行。我想展示一个解决方案,首先滚动到新插入的项目,然后显示详细视图。

虽然这不是我最喜欢的动画或流程。

该解决方案添加了一个额外的 ScrollViewReader,它似乎也可以完美地与 Forms 一起使用。我们要滚动到的视图是 NavigationLink(Label 不起作用,因为它在未显示时未实例化)。

注意

我偶尔会遇到一些故障,可能是Playgrounds。此外,在 Xcode 13 beta 4 上,控制台中发出警告,表明 NavigationLink 存在问题,我们应该提交错误报告。嗯...

我怀疑在执行滚动动画和随后的推送导航时可能存在问题。需要更多调查。

struct ContentView: View {
    @State var entries = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
    @State var currentSelection: Int? = nil

    var body: some View {
        NavigationView {
            ScrollViewReader { proxy in
                Form {
                    ForEach(entries.sorted(), id: \.self) { entry in
                        NavigationLink(
                            destination: DetailView(entry: entry),
                            tag: entry,
                            selection: $currentSelection,
                            label: { Text("The number \(entry)") }
                        )
                        .id("-\(entry)-")
                    }
                }
                .onChange(of: currentSelection) { newValue in
                    withAnimation {
                        if let currentSelection = self.currentSelection {
                            proxy.scrollTo("-\(currentSelection)-")
                        }
                    }
                }
                .toolbar {
                    ToolbarItem(placement: ToolbarItemPlacement.navigationBarLeading) { Button("Add low") {
                        let newEntry = (entries.min() ?? 1) - 1
                        entries.insert(newEntry, at: 1)
                        currentSelection = newEntry
                    } }
                    ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) { Button("Add high") {
                        let newEntry = (entries.max() ?? 50) + 1
                        entries.append(newEntry)
                        currentSelection = newEntry
                    } }
                    ToolbarItem(placement: ToolbarItemPlacement.bottomBar) {
                        Text("The current selection is \(String(describing: currentSelection))")
                    }
                }
            }
        }
    }
}

struct DetailView: View {
    let entry: Int
    var body: some View {
        Text("It's a \(entry)!")
    }
}