0

I have a List view with several items. All of then serve navigation purposes and all are Text. I cannot use NavigationLinks for this app.

That said, I am monitoring the list selection through a @State variable.

@State private var selection: String?

I print its value whever the view gets updated. It normally prints the unique id of every item when selecting different items of the list.

The problem is when trying to select the same item again: in this case the view does not get updated, and I don't see the debug message in my console.

I would like to be able to press on a list item to display a view, and then be able to press on it again, despite it already being selected because I have logic I would like to run (for example: manual view refresh).

How can I detect the re-selection of an already selected list item?

For any questions, please let me know. Thank you for your help!


Update 1

This is the code I am using:
Helper list item struct:

struct NavItem {
    let id: String
    let text: String
}

List:

struct NavList: View {
    
    @Binding private var selection: String?
    private var listItems: [NavItem]
    
    //note:
    //selection is a @State variable declared in a parent struct and passed here.
    //This is optional. I could declare it as a @state too.
    init(listItems: [NavItem], selection: Binding<String?>) { 
        self.listItems = listItems
        self._selection = selection
        debugPrint(self.selection)
    }
    
    var body: some View {
        
        List (selection: $selection) {
            ForEach(listItems, id:\.id) { item in 
                Text(item.text)
                    .tag(item.id)
            }
        }
    }
}

Note 1: I already know this problem can be approached by using the .onTapGesture modifier. This solution seems to not be optimal. I tried seeking help for it here.

Note 2: In the project, the struct that contains the list conforms to View. When that view is displayed, it has the .listStyle(SidebarListStyle()) modifier added. This might be relevant.

Lae
  • 832
  • 1
  • 13
  • 34
  • can you please show more code ? – Osman Oct 05 '21 at 22:40
  • @Osman I added code to my question. With this you should be able to reproduce the question after adding some example data as list items... – Lae Oct 05 '21 at 22:51
  • 1
    How is `.onTapGesture` not optimal? – Yrb Oct 05 '21 at 22:51
  • @Yrb As I asked earlier today in another question: https://stackoverflow.com/questions/69450904/swiftui-make-remaining-area-of-list-item-detect-clicks (which was __wrongfully__ closed as "already having an answer"), the `.onTapGesture` does not cover the entirety of the list. The highlight extends __around__ the selected item. This leaves room for the user to select the item, but not get a response from their action. I was unable to sovle this issue or get any help, thus, I dubbed it not optimal. – Lae Oct 05 '21 at 22:54
  • 1
    I saw that one. I am not sure you understood the link and the comments. Give me a minute. – Yrb Oct 05 '21 at 23:00

1 Answers1

2

there are a number of ways to do what you want, try this with .onTapGesture{..}

    List (selection: $selection) {
        ForEach(listItems, id: \.id) { item in
            Text(item.text)
                .tag(item.id)
                .padding(10)
                .listRowInsets(.init(top: 0,leading: 0, bottom: 0, trailing: 0))
                .frame(minWidth: 111, idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
                .onTapGesture {
                    print("----> " + item.text)
                }
        }
    }
    

or you can use a Button instead of onTapGesture;

    List (selection: $selection) {
        ForEach(listItems, id: \.id) { item in
            Button(action: { print("----> " + item.text) }) {
                Text(item.text)
            }
                .padding(10)
                .listRowInsets(.init(top: 0,leading: 0, bottom: 0, trailing: 0))
            .tag(item.id)
            .frame(minWidth: 111, idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
        }
    }

EDIT: full test code:

This is the code I used in my tests:

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
    }
}

struct NavItem {
    let id: String
    let text: String
}

struct ContentView: View {
    @State private var listItems: [NavItem] = [NavItem(id: "1", text: "1"),
                                              NavItem(id: "2", text: "2"),
                                              NavItem(id: "3", text: "3"),
                                              NavItem(id: "4", text: "4")]
    @State private var selection: String?
    
    var body: some View {
        List (selection: $selection) {
            ForEach(listItems, id:\.id) { item in
                Button(action: { print("----> " + item.text) }) {
                    Text(item.text)
                }
                .padding(10)
                .listRowInsets(.init(top: 0,leading: -5, bottom: 0, trailing: -5))
                .frame(minWidth: 111, idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
                .tag(item.id)
                .background(Color.red)  // <--- last modifier
            }
        }
    }
 }
  • He beat me to it. The trick to what you want is setting the frame of the clicked item. This expands the touch size. – Yrb Oct 05 '21 at 23:39
  • The first solution, as mentioed in the comments, is not optimal. This has already been attempted here (https://stackoverflow.com/questions/69450904/swiftui-make-remaining-area-of-list-item-detect-clicks?noredirect=1&lq=1) as well. The button has the same issue: when the item is selected, there is a small area left around the button that does not perform any action when clicked. Regardless, thank you very much for your contribution! ----- Tested the first solution: it produces the exact same result as the image in linked question. – Lae Oct 05 '21 at 23:39
  • 1
    updated my answer with a possible solution to the small area left around the button. – workingdog support Ukraine Oct 05 '21 at 23:44
  • Adding `.listRowInsets` modifier is indeed improving things! The top and bottom area is now covered by the item. There is a still a very slight area left on the two sides. Very clever! – Lae Oct 05 '21 at 23:48
  • 1
    works very well as is in my tests. You could try with a small negative value for the leading/trailing parameters. – workingdog support Ukraine Oct 05 '21 at 23:50
  • @workingdog I am currently tinkering with negative values for `.trailing` and `.leading` . It does not seem to work as well. `.offset` seems ineffective as well. – Lae Oct 05 '21 at 23:53
  • Are you really using the code that you are showing us? Because that works very well in my tests, including a -5 for leading and trailing values. – workingdog support Ukraine Oct 06 '21 at 00:08
  • @workingdog Yes, I am working with the code I have provided in this question to make sure there is absolutely nothing else interfering. On that note, the current state is this (if you add `.background(Color.red)` modifier): https://imgur.com/a/5IYM3ca - As you can see a small area on the sides remains. (Note: there is NO bottom overflow, this is from the item below!) – Lae Oct 06 '21 at 00:11
  • 1
    I've updated my answer with the full code I'm using. Your picture shows a icon and Home text, this is not from the code you provided. – workingdog support Ukraine Oct 06 '21 at 00:34
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/237860/discussion-between-lae-and-workingdog). – Lae Oct 06 '21 at 00:38
  • thanks, but I don't do chats. – workingdog support Ukraine Oct 06 '21 at 00:42