Search code examples
swiftuiwatchos

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?


Solution

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