How to toggle Image on click within Table?

157 views Asked by At

I want to display a table with a text and an image on each row. Clicking on the image should toggle the picture. The table is associated with an array of instances of a class, which has a boolean property for storing the state of the image.

enter image description here

Clicking seems to have no effect.

import SwiftUI

struct TestView: View {
    @State private var items = [
        Item(id: 1, text: "Eins", isClicked: false),
        Item(id: 2, text: "Zwei", isClicked: true),
        Item(id: 3, text: "Drei", isClicked: false)
    ]

    var body: some View {
        Table(items) { 
            TableColumn("Items") { item in
                HStack {
                    Text(item.text)
                    Button(action: {
                        item.isClicked.toggle()
                    }) {
                        Image(systemName: item.isClicked ? "checkmark.circle.fill" : "circle")
                    }
                }
            }
        }
    }
}

#Preview {
    TestView()
}

class Item: Identifiable {
    var id: Int
    var text: String
    var isClicked: Bool

    init(id: Int, text: String, isClicked: Bool) {
        self.id = id
        self.text = text
        self.isClicked = isClicked
    }
}

What did I do wrong?

3

There are 3 answers

0
lorem ipsum On BEST ANSWER

SwiftUI is highly dependent on being able to identify when something has changed, it is mostly done via Hashable, Identifiable and Equatable but reference types require a little more work.

For a class to work it must be an ObservableObject or an Observable.

https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro

The most compatible solution is to use a struct instead of a class.

struct Item: Identifiable, Hashable {
    var id: Int
    var text: String
    var isClicked: Bool

    init(id: Int, text: String, isClicked: Bool) {
        self.id = id
        self.text = text
        self.isClicked = isClicked
    }
}

Then the View will just need a minor adjustment.

struct TwoWayTable: View {
    @State private var items = [
        Item(id: 1, text: "Eins", isClicked: false),
        Item(id: 2, text: "Zwei", isClicked: true),
        Item(id: 3, text: "Drei", isClicked: false)
    ]

    var body: some View {
        Table($items) { //Enable two-way communication
            TableColumn("Items") { $item in
                HStack {
                    Text(item.text)
                    Button(action: {
                        $item.wrappedValue.isClicked.toggle() //Affect the wrapped value so State can trigger a redraw.
                    }) {
                        Image(systemName: item.isClicked ? "checkmark.circle.fill" : "circle")
                    }
                }
            }
        }
    }
}
0
Mentalist 302 On

The issue here is that SwiftUI's Table does not directly support interactive elements like buttons within cells. You should use List instead, which is designed to handle user interaction with its elements.

Try this:

import SwiftUI

struct TestView: View {
    @State private var items = [
        Item(id: 1, text: "Eins", isClicked: false),
        Item(id: 2, text: "Zwei", isClicked: true),
        Item(id: 3, text: "Drei", isClicked: false)
    ]

    var body: some View {
        List(items) { item in
            HStack {
                Text(item.text)
                Button(action: {
                    item.isClicked.toggle()
                }) {
                    Image(systemName: item.isClicked ? "checkmark.circle.fill" : "circle")
                }
            }
        }
    }
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}

class Item: Identifiable {
    var id: Int
    var text: String
    var isClicked: Bool

    init(id: Int, text: String, isClicked: Bool) {
        self.id = id
        self.text = text
        self.isClicked = isClicked
    }
}
0
workingdog support Ukraine On

If targeting iOS-17, try this approach using the Observation framework. When the item is changed, the view will refresh due to the observation.

@Observable class Item: Identifiable {  // <--- here
....
}

EDIT-1

If you are targeting ios-16+, then you could use this approach using ObservableObject.

Example code:

struct ContentView: View {
    var body: some View {
        TestView()
    }
}

struct TestView: View {
    @StateObject private var dataModel = DataModel()
    
    var body: some View {
        Table($dataModel.items) {
            TableColumn("Items") { $item in
                HStack {
                    Text(item.text)
                    Button(action: {
                        item.isClicked.toggle()
                    }) {
                        Image(systemName: item.isClicked ? "checkmark.circle.fill" : "circle")
                    }
                }
            }
        }
    }
}

class DataModel: ObservableObject {
    @Published var items: [Item] = [
        Item(id: 1, text: "Eins", isClicked: false),
        Item(id: 2, text: "Zwei", isClicked: true),
        Item(id: 3, text: "Drei", isClicked: false)
    ]
}

struct Item: Identifiable {
    var id: Int
    var text: String
    var isClicked: Bool
}