Bartłomiej Semańczyk
Bartłomiej Semańczyk

Reputation: 61774

Why My SwiftUI List does not update elements on the list?

This is my sample, completely possible to test example:

import SwiftUI

struct Category: Identifiable {
    var name: String
    var color: Color
    var id = UUID()
    init(name: String, color: Color) {
        self.name = name
        self.color = color
    }
}

struct CategoryWrapper: Identifiable {
    var id: UUID
    let category: Category
    let isSelected: Bool
    init(category: Category, isSelected: Bool) {
        self.category = category
        self.isSelected = isSelected
        self.id = category.id
    }
}

class ViewModel: ObservableObject {
    @Published var wrappers = [CategoryWrapper]()
    var selectedIdentifier = UUID()
    private var categories: [Category] = [
        Category(name: "PURPLE", color: .purple),
        Category(name: "GRAY", color: .gray),
        Category(name: "YELLOW", color: .yellow),
        Category(name: "BROWN", color: .brown),
        Category(name: "green", color: .green),
        Category(name: "red", color: .red),
    ]
    
    init() {
        reload()
    }
    
    func reload() {
        wrappers = categories.map { CategoryWrapper(category: $0, isSelected: $0.id == selectedIdentifier) }
    }
}

typealias CategoryAction = (Category?) -> Void

struct CategoryView: View {
    var category: Category
    @State var isSelected: Bool = false
    private var action: CategoryAction?
    init(category: Category, isSelected: Bool, action: @escaping CategoryAction) {
        self.category = category
        self.isSelected = isSelected
        self.action = action
    }
    
    var body: some View {
        Button {
            isSelected.toggle()
            action?(isSelected ? category : nil)
        } label: {
            Text(category.name)
                .font(.caption)
                .foregroundColor(.white)
                .background(isSelected ? category.color : .clear)
                .frame(width: 150, height: 24)
                .cornerRadius(12)
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    var body: some View {
        ScrollView {
            Text("Categories")
            ForEach(viewModel.wrappers) { wrapper in
                CategoryView(
                    category: wrapper.category,
                    isSelected: wrapper.isSelected
                ) { category in
                    viewModel.selectedIdentifier = category?.id ?? UUID()
                    viewModel.reload()
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

and the result is:

enter image description here

Every row view is tappable. Every tap should select current category and deselect the others. Why doesn't it work? When I tap on gray or yellow, previously selected rows are not deselected. Why?

Upvotes: 0

Views: 123

Answers (1)

RelativeJoe
RelativeJoe

Reputation: 5084

There are many problems with your code:

  1. Since you have single selection, selectedIdentifier should be optional;
  2. In CategoryView, you don't need to mark isSelected with @State because the list will reload itself after setting selectedIdentifier & calling reload():
struct Category: Identifiable {
    var name: String
    var color: Color
    var id = UUID()
    init(name: String, color: Color) {
        self.name = name
        self.color = color
    }
}

struct CategoryWrapper: Identifiable {
    var id: UUID
    let category: Category
    let isSelected: Bool
    init(category: Category, isSelected: Bool) {
        self.category = category
        self.isSelected = isSelected
        self.id = category.id
    }
}

class ViewModel: ObservableObject {
    @Published var wrappers = [CategoryWrapper]()
    var selectedIdentifier: UUID?
    private var categories: [Category] = [
        Category(name: "PURPLE", color: .purple),
        Category(name: "GRAY", color: .gray),
        Category(name: "YELLOW", color: .yellow),
        Category(name: "BROWN", color: .brown),
        Category(name: "green", color: .green),
        Category(name: "red", color: .red),
    ]
    
    init() {
        reload()
    }
    
    func reload() {
        wrappers = categories.map { CategoryWrapper(category: $0, isSelected: $0.id == selectedIdentifier) }
    }
}

typealias CategoryAction = (Category?) -> Void

struct CategoryView: View {
    var category: Category
    var isSelected: Bool = false
    private var action: CategoryAction?
    init(category: Category, isSelected: Bool, action: @escaping CategoryAction) {
        self.category = category
        self.isSelected = isSelected
        self.action = action
    }
    
    var body: some View {
        Button {
            action?(!isSelected ? category : nil)
        } label: {
            Text(category.name)
                .font(.caption)
                .foregroundColor(.white)
                .frame(width: 150, height: 24)
                .background(isSelected ? category.color : .clear)
                .cornerRadius(12)
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    var body: some View {
        ScrollView {
            Text("Categories")
            ForEach(viewModel.wrappers) { wrapper in
                CategoryView(
                    category: wrapper.category,
                    isSelected: wrapper.isSelected
                ) { category in
                    viewModel.selectedIdentifier = category?.id
                    viewModel.reload()
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Upvotes: 1

Related Questions