I have a list of items in a Form
in a NavigationView
, each having a detail-view that can be reached with NavigationLink
.
When I add a new element to the list, I want to show its detail-view. For that I use a @State var currentSelection
that the NavigationLink
receives as selection
, and each element has functions as the tag
:
NavigationLink(
destination: DetailView(entry: entry),
tag: entry,
selection: $currentSelection,
label: { Text("The number \(entry)") })
This works, and it follows the Apple docs and the best practises.
The surprise is, that it stops working when the list has more elements than fit on screen (plus ~2). Question: Why? And how can I work around it?
I made a minimal example to replicate the behaviour:
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)!")
}
}
(I ruled out that the number of elements is the core problem by reducing the list to 5 items and setting a padding on the label: label: { Text("The number \(entry).padding(30)") })
)
As you can see in the screen-recordings, after reaching the critical number of elements (either by prepending or appending to the list), the bottom sheet still shows that the currentSelection
is being updated, but no navigation is happening.
I used iOS 14.7.1, Xcode 12.5.1 and Swift 5.
This happens because lower items are not rendered, so in the hierarchy there's no NavigationLink
with such tag
I suggest you using an ZStack
+ EmptyView
NavigationLink
"hack".
Also I'm using LazyView here, thanks to @autoclosure
it lets me pass upwrapped currentSelection
: this will only be called when NavigationLink
is active, and this is happens when 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: $0) },
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() }
)
}
}
Check out more about LazyView, it helps often with NavigationLink
: in real apps destination may be a huge screen, and when you have a NavigationLink
in each cell SwiftUI will process all of them which may lead to lags