Search code examples
swiftui

NavigationLink in ScrollView doesn't handle or display like it does in a List


I've found that both List and Form are too limited for my needs, so I'm forced to build a traditional looking UI in a ScrollView.

When used in List, a NavigationLink, when tapped, will exhibit an animated highlight before the destination content is pushed on to the NavigationStack. Notably, the whole cell will be highlighted.

Problem: this behaviour does not occur when a NavigationLink is used in a ScrollView.

Firstly, .contentShape() must be used, so as to allow tapping anywhere within the cell, instead of just on the individual content.

But more annoyingly, the tap highlight doesn't affect the entire cell. It only affects the contents of the cell (e.g. Text, Image).

Before tap:

Before tap

On tap. Note that the cell background is unaffected:

On tap

Question: how can I ensure List cell-style tapping animation behaviour, on a NavigationLink in a ScrollView?

struct TestCellView: View {

    var body: some View {
        ScrollView {
            GroupBox {
                NavigationLink(destination: Text("Some detail view")) {
                    HStack {
                        Text("FAQ")
                        Spacer()
                        Image(systemName: "chevron.right")
                            .foregroundColor(.secondary)
                    }
                    .contentShape(Rectangle()) // Required to make whole button tappable.
                }
                .buttonStyle(.plain) // Remove system tinting on the Text and Image.
            }
        }
        .padding()
        .navigationTitle("Test")
        .navigationBarTitleDisplayMode(.inline)
    }
}


#Preview {
    NavigationStack {
        TestCellView()
            .preferredColorScheme(.dark)
    }
}

Solution

  • I would write a custom ButtonStyle, where you can check isPressed and change the button background. In fact, I'd put the whole HStack in the button so that you don't have to write the Image and Spacer for every NavigationLink.

    extension ButtonStyle where Self == NavigationLinkListRow {
        static var navigationLinkListRow: Self { .init() }
    }
    
    struct NavigationLinkListRow: ButtonStyle {
        func makeBody(configuration: Configuration) -> some View {
            HStack {
                configuration.label
                Spacer()
                Image(systemName: "chevron.right")
                    .foregroundColor(.secondary)
            }
            .padding()
            .background(
                // .background.tertiary and .background.secondary are what I found to look nice
                // I recommend experimenting with other ShapeStyles too
                configuration.isPressed ? AnyShapeStyle(.background.tertiary) : AnyShapeStyle(.background.secondary),
                in: RoundedRectangle(cornerRadius: 8)
            )
        }
    }
    
    struct ContentView: View {
        var body: some View {
            NavigationStack {
                ScrollView {
                    NavigationLink {
                        Text("Some detail view")
                    } label: {
                        // no need for the Spacer and Image here!
                        Text("FAQ")
                    }
                    // or simply:
                    // NavigationLink("FAQ") {
                    //     Text("Some detail view")
                    // }
                }
                .padding()
                .navigationTitle("Test")
                .navigationBarTitleDisplayMode(.inline)
                .buttonStyle(.navigationLinkListRow)
            }
        }
    }