swiftuiswiftui-list

How to re-add the tap animation to a list customized with .listRowBackground()


I have this List which I have customised to fit my app design, one of the things I added is .listRowBackground(), this gives me the ability to change a row's background color.

With this change comes an issue, though. The default animation for tapping a row in a list, which is a subtle highlight, doesn't show anymore.

Is there any way to re-enable this animation, or another way to customize the color of the list's background which keeps the animation?

Here's some code for reference:

    List {
        Button(action: {
            // do something
        }) {
            HStack {
                Text("Title 1")
            }
        }
        .listRowBackground(StyleManager.Colors.secondary)
        
        NavigationLink(value: "Title 2") {
            Text("Title 2")
        }
        .listRowBackground(StyleManager.Colors.secondary)
    }
    .someOtherStyles(likeShadows, fonts, etc)

Solution

  • You could try using a custom ButtonStyle for the buttons. The background color can then depend on whether the button is pressed. This information is provided by the configuration received by the button style.

    struct CustomListButton: ButtonStyle {
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
                .padding(.horizontal, 20)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
                .contentShape(Rectangle())
                .background(configuration.isPressed ? .yellow : .clear)
        }
    }
    

    Example use:

    struct ContentView: View {
        var body: some View {
            List(1...10, id: \.self) { n in
                Button("Row \(n)") {
                    print("Row \(n) tapped")
                }
                .buttonStyle(CustomListButton())
                .listRowBackground(Rectangle().fill(.background))
                .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
            }
            .listStyle(.insetGrouped)
            .scrollContentBackground(.hidden)
            .background(Color(white: 0.8))
        }
    }
    

    Animation

    Here is how the same button style can be used in connection with the solution I was suggesting in answer to your previous question:

    List(1...10, id: \.self) { n in
        Button("Row \(n)") {
            print("Row \(n) tapped")
        }
        .buttonStyle(CustomListButton())
        .modifier(CustomListRowStyle(
            foregroundColor: .green,
            shadowColor: .yellow,
            shadowOffsetY: 3,
            position: n == 1 ? .first : (n == 10 ? .last : .middle)
        ))
    }
    .listStyle(.grouped)
    .scrollContentBackground(.hidden)
    .background(Color(white: 0.8))
    

    Animation


    EDIT Following up on your comment, it seems that in the context of a List, tap gestures do not trigger a change to configuration.isPressed. When the button is not in a List, the flag does get toggled on tap.

    As a workaround, you could create a wrapper for the Button and then set your own flag when the button is tapped. The flag could be reset again after a short delay.

    Here is an updated version of the code above that works this way.

    struct CustomListButtonStyle: ButtonStyle {
        let tapped: Bool
    
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
                .padding(.horizontal, 20)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
                .contentShape(Rectangle())
                .background(configuration.isPressed || tapped ? .yellow : .clear)
        }
    }
    
    struct CustomListButton: View {
        let label: String
        let action: () -> Void
        @State private var tapped = false
    
        var body: some View {
            Button(label) {
                tapped = true
                action()
                Task { @MainActor in
                    try? await Task.sleep(for: .milliseconds(50))
                    tapped = false
                }
            }
            .buttonStyle(CustomListButtonStyle(tapped: tapped))
            .transaction { trans in
                trans.disablesAnimations = true
            }
            .listRowBackground(Rectangle().fill(.background))
            .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
        }
    }
    
    struct ContentView: View {
        var body: some View {
            List(1...10, id: \.self) { n in
                CustomListButton(label: "Row \(n)") {
                    print("Row \(n) tapped")
                }
            }
            .listStyle(.insetGrouped)
            .scrollContentBackground(.hidden)
            .background(Color(white: 0.8))
        }
    }