How to detect taps on a List cell row in SwiftUI?
Asked Answered
U

3

18

Given a basic List with Text, how can i make the whole "cell" from left side of the screen to right, tappable in a List, not just the "Hello world" text?

    List {
         VStack {
             Text("Hello world")
         }    
         .contentShape(Rectangle())
         .onTapGesture {
            print("Tapped cell")  // This only triggers when you directly tap the Text
         }
     }
Uis answered 12/7, 2021 at 11:27 Comment(0)
J
28

Add a Button and entire cell is tappable now:

VStack {
    Button(action: {
        print("Tapped")
    }) {
        Text("Hello world")
    }
}
Jamieson answered 12/7, 2021 at 11:37 Comment(2)
Tip: you can use .buttonStyle(.plain) to disable the blue tint – Oui
@Oui using a plain button style in an inset grouped list doesn't work for me and the whole list row area doesn't respond to taps anymore. – Gape
S
20

Actually, all you really need to do is make sure the entire cell is filled with content. Changing the VStack to an HStack and adding a Spacer() will give you what you want.

    List {
         HStack {
             Text("Hello world")
             Spacer()
         }
         .contentShape(Rectangle())
         .onTapGesture {
            print("Tapped cell")  // This triggers when you tap anywhere in the cell
         }
     }
Stilted answered 26/7, 2022 at 19:41 Comment(1)
Using .onTapGesture makes degrades accessibility because the row doesn't register as a button in VoiceOver – Upspring
S
4

I think the best solution (from both a code, accessibility and UI point of view) is to use the selection: parameter of List.

This has the main benefit of automatically highlighting the cell on tap, and handling the tap gesture states correctly (triggering the selection change only on touch up if the finger is still inside the cell):

import SwiftUI

struct Fruit: Hashable, Identifiable {
    let id = UUID()
    let name: String
}

struct ContentView: View {
    let fruits = [
        Fruit(name: "🍌 Banana"),
        Fruit(name: "🍎 Apple"),
        Fruit(name: "🍊 Orange")
    ]

    @State private var selectedFruit: Fruit.ID?

    var body: some View {
        NavigationStack {
            List(fruits, selection: $selectedFruit) { fruit in
                Text(fruit.name)
                    .foregroundStyle(Color(uiColor: .label))
                    .listRowBackground(fruit.id == selectedFruit ? Color(uiColor: .systemGray4) : nil)
            }
            .navigationTitle("Fruits")
        }
        .onChange(of: selectedFruit) { _, newValue in
            if let newValue {
                print("Touched \(fruits.first(where: { $0.id == newValue })!.name)")
            }
        }
    }
}

We unfortunately need these 2 lines :

.foregroundStyle(Color(uiColor: .label))
.listRowBackground(fruit.id == selectedFruit ? Color(uiColor: .systemGray4) : nil)

Because otherwise the selected cell will use your app's tint color as its background when an hardware keyboard is connected.


If you want the cell do immediately lose its selected state after tap, you can modify .onChange like so :

.onChange(of: selectedFruit) { _, newValue in
    if let newValue {
        print("Touched \(fruits.first(where: { $0.id == newValue })!.name)")

        Task {
            try? await Task.sleep(for: .milliseconds(80))
            selectedFruit = nil
        }
    }
}

(adjust the delay as you see fit)


This is the final result:

demo of the sample code on iPad simulator

Subdiaconate answered 15/3 at 14:51 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.