Search code examples
swiftuiswiftui-table

How to toggle Image on click within Table?


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?


Solution

  • 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")
                        }
                    }
                }
            }
        }
    }