SwiftUI NavigationLink in List on iOS 14

Hi everyone,

I'm experiencing some unwanted behaviour in my app on iOS 14. When tapping on a NavigationLink in a List, the "Cell" keeps being highlighted even when navigating back from the detail view. Only if I selected another Cell, the highlighting disappears. But therefore the other Cell is now highlighted.

Is there any way to remove this behaviour? Running on iOS 13, everything was normal. The only workaround is to attach .id(UUID()) to the NavigationLink. But this will cause a flickering of all Cells when navigating back from the detail view and losing the scrolling position of the list.

Thanks for any help or hints.


The only workaround is to attach .id(UUID()) to the NavigationLink.

That's what I was going to suggest … It is not a trick, it is a good practice so that the List knows which objects to update.
So, that seems to be the desired behaviour (apparently not the one you expect).
Maybe the reason is to keep the present selection halite if you use split views.

If you do the same (attach .id(UUID())) in iOS 13, do you get the same behaviour ?

Feel free to post a bug report (or enhancement suggestion). SwiftUI is still in evolution and there are some corners to round…
Thanks for the quick response!

Attaching .id(UUID()) on iOS 13 has no effect, i.e. no flickering. The behaviour in iOS 14 is very strange. I have the issue in dynamic and static Cells, though with one exception: in a pure static list.

I guess I have to find a way to selectively change only the ID of the NavigationLink active.

The problem is, that my app is in production since march, build mostly upon SwiftUI. Now iOS 14 breaks a lot which might forces me to switch to UIKit.

Also results of a @FetchedRequest update in iOS 13 in Lists automatically with animation but in iOS 14 they don't anymore :(
I have the same Issue as @danmedia93  and another one as well (a optional Binding, that worked in iOS13, is always nil the first it is used in iOS14. So I had to come up with a pretty ugly workaround for iOS14, that it is not optional anymore and the State it is referring to has some stupid value, so the app doesn't crash on iOS14 devices)

I tried adding the .buttonStyle(PlainButtonStyle())-modifier to the Row and the List, and of course
Code Block
.onAppear {
  UITableViewCell.appearance().selectionStyle = .none
}

to the list.
Nothing worked like it should in iOS14. This changes only showed some effect in iOS14 @Claude31 
I have noticed this highlight issue happens when List View is inside VStack or any other container, which is mostly the case in most of the apps.
For me .id(UUID()) didnt work
After spending ton of hours i ended up using LazyVStack inside ScrollView
Yes sometime cells stay highlighted for some reason.

For example I found that this problem could appear if you add a padding and a background color to a list.
This list for example should have the problem and the rows will stay highlighted after back is pressed.
Code Block
struct TestListView: View {
var body: some View {
NavigationView {
List {
NavigationLink(
destination: Text("Detail 1"),
label: {
Text("Show detail 1")
}
)
NavigationLink(
destination: Text("Detail 2"),
label: {
Text("Show detail 2")
}
)
}
.padding(.top, 10)
.background(Color.blue)
}
}
}


You can try to remove some of the list modifiers.
If you still have the problem you can try with Introspect.
https://github.com/siteline/SwiftUI-Introspect

The above code could be fixed in this way:
Code Block
struct TestListView: View {
@State private var tableView: UITableView?
private func deselectRows() {
if let tableView = tableView, let selectedRow = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRow, animated: true)
}
}
var body: some View {
NavigationView {
List {
NavigationLink(
destination: Text("Detail 1").onAppear {
deselectRows()
},
label: {
Text("Show detail 1")
}
)
NavigationLink(
destination: Text("Detail 2").onAppear {
deselectRows()
},
label: {
Text("Show detail 2")
}
)
}
.padding(.top, 10)
.background(Color.blue)
.introspectTableView(customize: { tableView in
self.tableView = tableView
})
}
}
}

A little bit ugly but it works.

This seems to happen with navigating in presented views. Legit bugs.
I had to take a different path to solve this. This is my workaround.

Code Block Swift
struct WrappedRow<Content>: View where Content: View {
    
    @Binding var isSelected: Bool?
    let highlightColor: Color
    let content: Content
    
    @State private var isHighlighted: Bool = false
    
    var body: some View {
        ZStack {
            content
            GeometryReader { g in
                Color.clear
                    .contentShape(Rectangle())
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .onChanged { currentValue in
                                isHighlighted = true
                            }
                            .onEnded { gestureValue in
                                let releasedOutsideBounds = gestureValue.location.y > g.size.height
                                    gestureValue.location.y < g.size.height - g.size.height
                                    gestureValue.location.x > g.size.width
                                    || gestureValue.location.x < g.size.width - g.size.width
                                
                                isSelected = !releasedOutsideBounds
                                isHighlighted = false
                            }
                    )
                if isHighlighted {
                    highlightColor
                        .padding(-max(UIScreen.main.bounds.width, UIScreen.main.bounds.height))
                        .opacity(0.35)
                }
            }
        }
    }
}


I use it like this:

Code Block Swift
List {
            Section {
                NavigationLink(destination: Settings(), tag: true, selection: $showSettings) {
                    WrappedRow(isSelected: $showReset, highlightColor: .gray, content: SettingsRow())
                }
            }
...
        }
        .listStyle(InsetGroupedListStyle())



I have found that adding an offset AND background to the list causes the problem.

Simple code to show the problem (this has been submitted to Feedback).

Code Block
import SwiftUI
struct ContentView: View {
    var body: some View {
            NavigationView {
                List {
                    NavigationLink ("Test1", destination: Text("Test1"))
                    NavigationLink ("Test2", destination: Text("Test2"))
                }
                .background(Color(UIColor.secondarySystemBackground))
                .offset(x: 0, y: 20)
            }
        }
}

This is definitely a bug in List, for now, my work-around is refreshing the List by changing the id, like this:
Code Block swift
struct YourView: View {
@State private var selectedItem: String?
@State private var listViewId = UUID()
var body: some View {
List(items, id: \.id) {
NavigationLink(destination: Text($0.id),
tag: $0.id,
selection: $selectedItem) {
Text("Row \($0.id)")
}
}
.id(listViewId)
.onAppear {
if selectedItem != nil {
selectedItem = nil
listViewId = UUID()
}
}
}
}

I thought I had tried everything until I came across amirfromden Haag's answer. It worked beautifully! Thanks so much!

Go to the top navigation View change its navigation style to

.navigationViewStyle(StackNavigationViewStyle())

This is happening because the navigation view needs to know the selection for the landscape mode. That's why it stays selected until you pick another element.

After spending a whole day on this, I found a way that seems to be working on iOS 14 by wrapping a NavigationLink inside a button's label and triggering it programmatically using the button's action. The highlight color seems to be clearing right after the tap (because it's a button that is being tapped, not the NavigationLink).

The trick is that the NavigationLink uses an EmptyView() as it's content, so it's there to define the navigation but not to trigger it itself and let the button do it programmatically.

I wrote a custom NavLink struct that accepts similar parameters as the NavigationLink and does the wrapping for you:

struct NavLink<Content: View, Destination: View>: View {

    var destination: Destination
    var tag: String
    @Binding var selection: String?
    @ViewBuilder var content: Content

    var body: some View {
        Button(action: {
            selection = tag
        }, label: {
            ZStack {
                NavigationLink(destination: destination, tag: tag, selection: $selection) {
                    EmptyView()
                }
                .isDetailLink(true)
                content
            }
        })
    }
}

Here's how it can be used:

NavLink(destination: Text("Destination"), tag: "tag", selection: $selection) {
    Text("List item goes here")
}

(I've included an .isDetailLink modifier to the NavigationLink, in case you don't need it you can take it away or add a new argument to set it).

Seems to be working well for me, please share your feedback and any potential ways that it can be improved.

SwiftUI NavigationLink in List on iOS 14
 
 
Q