37

In iOS 14, it appears that NavigationLinks do not become deselected after returning in a Form context. This is also true for Form Pickers and anything else that causes the presentation of another View from a list (giving a highlight context to the presenting cell).

I didn't notice this behaviour in iOS 13.

Is there a way to 'deselect' the highlighted row once the other view is dismissed?

Example code:

struct ContentView: View {

    var body: some View {
        Form {
            NavigationLink(destination: Text("Detail")) {
                Text("Link")
            } 
        }
    }

}

(Different) Example visual:

Example

Bradley Mackey
  • 6,777
  • 5
  • 31
  • 45
  • 2
    This usually happens when you have multiple NavigationViews in your navigation stack. Make sure that your view is embedded in only one NavigationView. – pawello2222 Sep 17 '20 at 11:27
  • 4
    I have the exact same problem but wasn't able to fix it. Also, I only have one NavigationView @pawello2222 – leonboe1 Sep 17 '20 at 14:39
  • @leonboe1 Check my answer to see if it helps! – Bradley Mackey Sep 17 '20 at 16:10
  • @BradleyMackey Unfortunately, it doesn't help me. It works for one page, yes, but I have NavigationLinks on a Page A which are leading to Page B, and on Page B there are ones leading to another Page C etc. I cannot use NavigationView multiple times. – leonboe1 Sep 17 '20 at 17:45
  • Seems to be a problem with sheet. See https://stackoverflow.com/questions/63945077/swiftui-multiple-navigationlinks-in-form-sheet-entry-stays-highlighted – leonboe1 Sep 17 '20 at 19:34

11 Answers11

24

In my case this behaviour appeared when using any Viewcontent (e.g. Text(), Image(), ...) between my NavigationView and List/Form.

var body: some View {
    
    NavigationView {
        VStack {
            Text("This text DOES make problems.")
            List {
                NavigationLink(destination: Text("Doesn't work correct")) {
                    Text("Doesn't work correct")
                }
            }
        }
    }
}

Putting the Text() beneath the List does not make any problems:

var body: some View {
    
    NavigationView {
        VStack {
            List {
                NavigationLink(destination: Text("Does work correct")) {
                    Text("Does work correct")
                }
            }
            Text("This text doesn't make problems.")
        }
    }
}

This is definitely a XCode 12 bug. As more people report this, as earlier it gets resolved.

Kuhlemann
  • 3,066
  • 3
  • 14
  • 41
20

I have also run into this issue and believed I found the root cause in my case.

In my case I had.a structure like the following:

struct Page1View: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink("Page 2", destination: Page2View())
            }
                .listStyle(GroupedListStyle())
                .navigationBarTitle("Page 1")
        }
    }
}

struct Page2View: View {
    var body: some View {
        List {
            NavigationLink("Page 3", destination: Text("Page 3"))
        }
            .listStyle(GroupedListStyle())
            .navigationBarTitle("Page 2")
    }
}

This issue would occur on the NavigationLink to Page 3. In the console output this error was showing when that link was used:

2021-02-13 16:41:00.599844+0000 App[59157:254215] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.

I discovered that I needed to apply .navigationViewStyle(StackNavigationViewStyle()) to the NavigationView and this solved the problem.

I.e.

struct Page1View: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink("Page 2", destination: Page2View())
            }
                .listStyle(GroupedListStyle())
                .navigationBarTitle("Page 1")
        }
            .navigationViewStyle(StackNavigationViewStyle())
    }
}

David
  • 732
  • 1
  • 6
  • 15
10

Been fighting this issue half day today and came to this post that helped me to understand that issue appears if Text, Button or something else placed between NavigationView and in my case List. And I found solution that worked for me. Just add .zIndex() for the item. .zIndex() must be higher than for List Tried with Xcode 12.5.

var body: some View {
    NavigationView {
        VStack {
            Text("This text DOES make problems.")
                .zIndex(1.0)
            List {
                NavigationLink(destination: Text("Doesn't work correct")) {
                    Text("Doesn't work correct")
                }
            }
        }
    }
}
Sangsom
  • 373
  • 5
  • 9
7

I did a bit more tinkering, it turns out this was caused due by having the UIHostingController being nested in a UINavigationController and using that navigation controller. Changing the navigation stack to use a SwiftUI NavigationView instead resolved this issue.

Similar to what @pawello2222 says in the question comments, I think the underlying cause is something to do with SwiftUI not understanding the proper navigation hierarchy when the external UINavigationController is used.

This is just one instance where this is fixed though, I'm still experiencing the issue in various other contexts depending on how my view is structured.

I've submitted an issue report FB8705430 to Apple, so hopefully this is fixed sometime soon.

Before (broken):

struct ContentView: View {
    var body: some View {
        Form {
             NavigationLink(destination: Text("test")) {
                 Text("test")
             }
        }
    }
}

// (UIKit presentation context)
let view = ContentView() 
let host = UIHostingController(rootView: view)
let nav = UINavigationController(rootViewController: host)
present(nav, animated: true, completion: nil)

After (working):

struct ContentView: View {
    var body: some View {
        NavigationView {
            Form {
                NavigationLink(destination: Text("test")) {
                    Text("test")
                }
            }
        }
    }
}

// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
present(host, animated: true, completion: nil)
Bradley Mackey
  • 6,777
  • 5
  • 31
  • 45
  • Just confirming that it used to work (and still works without issues) on iOS 13.7. – Tobias Timpe Oct 16 '20 at 07:36
  • 2
    I have the same issue but unfortunately this won't work because my initial view controller is UIKit table view and is the root view controller of a UINavigationController. When selecting a row, I push a view controller in which I create an instance of a UIHostingController wrapping my SwiftUI form and add it as a child view controller. If I give my form its own NavigationView, I will end up with 2 nav bars coming from 2 navigation controllers. I tried subclassing UIHostingController instead of instantiating it but it didn't work. I wish there were a clear fix for this. – HBR Mar 28 '21 at 17:57
  • There are even more problems with SwiftUI views inside UINavigationController. For example the navigation title and navigation bar buttons plop in after the navigation transition is done, instead of animating in like native UIKit ViewControllers do. This essentially means SwiftUI is not usable in a hybrid UIKit/SwiftUI app and i decided to rip it out again and replace it with regular UIKit... – Moritz Mahringer Apr 12 '21 at 14:02
7

This is definitely a bug in List, for now, my work-around is refreshing the List by changing the id, like this:

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 made a modifier based on this that you can use:

struct RefreshOnAppearModifier<Tag: Hashable>: ViewModifier {
    @State private var viewId = UUID()
    @Binding var selection: Tag?
    
    func body(content: Content) -> some View {
        content
            .id(viewId)
            .onAppear {
                if selection != nil {
                    viewId = UUID()
                    selection = nil
                }
            }
    }
}

extension View {
    func refreshOnAppear<Tag: Hashable>(selection: Binding<Tag?>? = nil) -> some View {
        modifier(RefreshOnAppearModifier(selection: selection ?? .constant(nil)))
    }
}

use it like this:

List { ... }
  .refreshOnAppear(selection: $selectedItem)
Amir Khorsandi
  • 3,542
  • 1
  • 34
  • 38
3

I managed to solve it by adding ids to the different components of the list, using binding and resetting the binding on .onDisappear

struct ContentView: View {
    @State var selection: String? = nil

    var body: some View {
        NavigationView {
            VStack {
                Text("Hello, world!")
                    .padding()
                List {
                    Section {
                        NavigationLink( destination: Text("Subscreen1"), tag: "link1", selection: $selection ) {
                            Text("Subscreen1")
                        }.onDisappear {
                            self.selection = nil
                        }
                        NavigationLink( destination: Text("Subscreen2"), tag: "link2", selection: $selection ) {
                            Text("Subscreen2")
                        }.onDisappear {
                            self.selection = nil
                        }
                    }.id("idSection1")
                }
                .id("idList")
            }
        }
    }
}
1

I've also run into this issue and it seemed related to sheets as mentioned here.

My solution was to swizzle UITableView catch selections, and deselect the cell. The code for doing so is here. Hopefully this will be fixed in future iOS.

Kyle
  • 17,317
  • 32
  • 140
  • 246
0

Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView fixed it for me.

Suggested in this thread: https://developer.apple.com/forums/thread/660468

David
  • 46
  • 1
  • 3
0

This is my solution to this issue.

// This in a stack in front of list, disables large navigation title from collapsing by disallowing list from scrolling on top of navigation title
public struct PreventCollapseView: View { 

    @State public var viewColor: Color?

    public init(color: Color? = nil) {
        self.viewColor = color
    }
    
    public var body: some View {
        Rectangle()
            .fill(viewColor ?? Color(UIColor(white: 0.0, alpha: 0.0005)))
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 1)
    }
}

// handy modifier..
extension List {
    
    public func uncollapsing(_ viewUpdater: Bool) -> some View {
        
        VStack(spacing: 0.0) {
            Group {
                PreventCollapseView()
                self
            }.id(viewUpdater)
        }
    }
}

struct TestView: View {

    @State var updater: Bool = false
    
    var body: some View {

        List {
            Text("Item one")
            Text("Item two")
            Text("Manually refresh")
                .onTapGesture { DispatchQueue.main.async { updater.toggle() } }
                .onAppear { print("List was refreshed") }
        }
        .uncollapsing(updater)
        .clipped()
        .onAppear { DispatchQueue.main.async { updater.toggle() }} // Manually refreshes list always when re-appearing/appearing

    }
}

Add a NavigationView, configure for largeTitle, and embed TestView and it's all set. Toggle updater to refresh.

jake1981
  • 303
  • 3
  • 11
-1

Having the same Problem. The weird thing is, that the exact same code worked in iOS13. I'm having this issue with a simple list:

struct TestList: View {
    let someArray = ["one", "two", "three", "four", "five"] 
    var body: some View {
        List(someArray, id: \.self) { item in 
                NavigationLink(
                    destination: Text(item)) {
                    Text(item)
                }.buttonStyle(PlainButtonStyle()) 
        }.navigationBarTitle("testlist") 
    }
}

This is embedded in:

struct ListControllerView: View {
    @State private var listPicker = 0
    var body: some View {
        NavigationView{
            Group{
                VStack{
                    Picker(selection: $listPicker, label: Text("Detailoverview")) {
                        Text("foo").tag(0)
                        Text("bar").tag(1)
                        Text("TestList").tag(2)
                    }

This is inside a Tabbar.

phil Schn
  • 17
  • 3
-1

This is the workaround I've been using until this List issue gets fixed. Using the Introspect library, I save the List's UITableView.reloadData method and call it when it appears again.

import SwiftUI
import Introspect

struct MyView: View {

    @State var reload: (() -> Void)? = nil

    var body: some View {
        NavigationView {
            List {
                NavigationLink("Next", destination: Text("Hello"))
            }.introspectTableView { tv in
                self.reload = tv.reloadData
            }.onAppear {
                self.reload?()
            }
        }
    }
}
WaltersGE1
  • 813
  • 7
  • 26
  • Unfortunately this no longer works, List is now some kind of collectionview and even when introspecting a collection view- this still doesn't seem to work. I found another way to archieve this and it's posted on this thread. – jake1981 Sep 22 '22 at 09:07