9

I have an issue with Xcode 12 / iOS 14. Using multiple NavigationLinks in a sheet with NavigationView leads to NavigationLink entries staying highlighted after going back a page. This is not only a problem with the simulator. See the attached GIF:

enter image description here

Does anybody know how to fix this?

Similar question: SwiftUI - NavigationLink cell in a Form stays highlighted after detail pop (but that's not the problem here).

struct ContentView: View {
    
    var body: some View {
        Text("")
            .sheet(isPresented: .constant(true), content: {
                NavigationView {
                    Form {
                        Section {
                            NavigationLink("Link to ViewB", destination: ViewB())
                        }
                    }
                    .navigationBarTitle("ViewA")
                }
            }) 
    }
}

struct ViewB: View {
    @State var selection = 0
    let screenOptions = ["a", "b", "c"]
    var body: some View{
        Form {
            Section {
                NavigationLink("Link to ViewC", destination: ViewC())
            }
        }
        .navigationBarTitle("ViewB")
    }
}

struct ViewC: View {
    var body: some View{
        Form {
            Section {
                Text("Test")
            }
        }
        .navigationBarTitle("ViewC")
    }
}

leonboe1
  • 1,004
  • 9
  • 27

2 Answers2

1

I've also run into this problem when using a NavigationLink inside a sheet. My solution on iOS 14 has been too Swizzle didSelectRowAt: of UITableView. When the row is selected, I deselect it. There is more code for detecting if its in a sheet, etc, but this is the basic, get it working code:

extension UITableView {
    
    @objc static func swizzleTableView() {
        
        guard self == UITableView.self else {
            return
        }
        
        let originalTableViewDelegateSelector = #selector(setter: self.delegate)
        let swizzledTableViewDelegateSelector = #selector(self.nsh_set(delegate:))
        
        let originalTableViewMethod = class_getInstanceMethod(self, originalTableViewDelegateSelector)
        let swizzledTableViewMethod = class_getInstanceMethod(self, swizzledTableViewDelegateSelector)
        
        method_exchangeImplementations(originalTableViewMethod!,
                                       swizzledTableViewMethod!)
    }
    
    @objc open func nsh_set(delegate: UITableViewDelegate?) {
        nsh_set(delegate: delegate)
        
        guard let delegate =  delegate else { return }
        
        let originalDidSelectSelector = #selector(delegate.tableView(_:didSelectRowAt:))
        let swizzleDidSelectSelector = #selector(self.tableView(_:didSelectRowAt:))
        
        let swizzleMethod = class_getInstanceMethod(UITableView.self, swizzleDidSelectSelector)
        let didAddMethod = class_addMethod(type(of: delegate), swizzleDidSelectSelector, method_getImplementation(swizzleMethod!), method_getTypeEncoding(swizzleMethod!))
        
        if didAddMethod {
            let didSelectOriginalMethod = class_getInstanceMethod(type(of: delegate), NSSelectorFromString("tableView:didSelectRowAt:"))
            let didSelectSwizzledMethod = class_getInstanceMethod(type(of: delegate), originalDidSelectSelector)
            if didSelectOriginalMethod != nil && didSelectSwizzledMethod != nil {
                method_exchangeImplementations(didSelectOriginalMethod!, didSelectSwizzledMethod!)
            }
        }
    }
    
    @objc open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.tableView(tableView, didSelectRowAt: indexPath)

        // This is specifically to fix a bug in SwiftUI, where a NavigationLink is
        // not de-selecting itself inside a sheet.
        tableView.deselectRow(at: indexPath,
                              animated: true)
        
    }
}

(Original swizzle code is from https://stackoverflow.com/a/59262109/127853), this code sample just adds the deselectRow call.)

Don't forget to call UITableView.swizzleTableView() somewhere such as application:didFinishLaunchingWithOptions:

Kyle
  • 17,317
  • 32
  • 140
  • 246
  • this crashes on iPad for me, on iPhone it works fine. Did you experience something similar? – dehlen Dec 12 '20 at 00:50
  • @dehlen I have added additional code in the `nsh_set` method to detect if the delegate is a SwiftUI class and only replace the method if so because I was having issues on non-swiftUI code (and it's not needed on non SwiftUI code). I'm actively using this code in https://apps.apple.com/app/appwage/id834352667#?platform=iphone. – Kyle Dec 14 '20 at 13:51
  • First of all thanks for getting back to me and for sharing your solution with us. On an iPhone this works for me. But running it on iPad results in an endless loop eventually crashing the application. Since I can't upload screenshots in a comment here is a link showing the debugger. As you can see it crashes on running the original implementation of `didSelectRow`. This is on any iPad running 14.2 with Xcode 12.2.0: https://cln.sh/ryVMcw – dehlen Dec 14 '20 at 15:21
1

Add the following modifier to your NavigationView to set navigation view style and fix this issue:

.navigationViewStyle(StackNavigationViewStyle())

Explanation:

Default style is DefaultNavigationViewStyle(), from documentation: "The default navigation view style in the current context of the view being styled".

For some reason this will pick up DoubleColumnNavigationViewStyle instead of StackNavigationViewStyle on iPhone, if you set style explicitly it behaves as expected.

Mirko
  • 2,231
  • 2
  • 21
  • 17