swiftmacosswiftui

How to make a row in a .grouped style Form into a button?


I am writing a .grouped-styled Form on macOS that allows users to edit a list of items. Let's say

struct ContentView: View {
    @State private var people = [
        Person(name: "Tom"),
        Person(name: "Dick"),
        Person(name: "Harry"),
    ]
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    ForEach($people) { $person in
                        TextField("Name", text: $person.name)
                    }
                    Button("New Person") {
                        people.append(Person(name: ""))
                    }
                }
            }
            .formStyle(.grouped)
        }
    }
}

struct Person: Identifiable {
    let id = UUID()
    var name: String
}

This produces

enter image description here

I don't like how the button at the end looks. I would like the entirety of that "list row" to be a clickable button (much like how Buttons in Lists on iOS look). How can I do that?

The closest I have found is a NavigationLink.

NavigationLink("New Person") {
    EmptyView()
}

produces

enter image description here

Now the whole "list row" is clickable, but I cannot run an arbitrary action. I can only navigate to another view.


Solution

  • To make it look like a NavigationLink, it requires a custom button style.

    When using .grouped form style, In order to make the highlight on tap cover the entire row, a negative padding needs to be applied to the button, and a positive padding on the button label:

    //NavigationLink style button
    Button {
        people.append(Person(name: ""))
    } label: {
        HStack {
            Text("New Person")
                .frame(maxWidth: .infinity, alignment: .leading)
            Spacer()
            Image(systemName: "chevron.right")
                .imageScale(.small)
                .foregroundStyle(.tertiary)
        }
        .padding(10)
    }
    .buttonStyle(NavigationLinkButtonStyle())
    .padding(-10)
    
    //Custom button style
    struct NavigationLinkButtonStyle: ButtonStyle {
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
                .contentShape(Rectangle())
                .background(
                    configuration.isPressed
                    ? Color(NSColor.separatorColor)
                    : Color.clear
                )
        }
    }