You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI实现分类单选:如何确保仅一个Category的isActive为true

Fixing Single Selection for Your MVVM Category View

Great question! The issue of multiple categories being selectable at once happens because you're managing the isActive state individually on each Category model. Let's adjust your structure to enforce single selection while keeping your MVVM pattern intact—you don't necessarily need to delete the isActive property, but a cleaner approach is to move selection state management to your parent view model.

The key idea is to have a single source of truth for the selected category in your CategoriesViewModel, rather than letting each Category track its own active state. Here's how to implement this step by step:

1. Simplify the Category Model

First, remove the isActive property from your Category struct—this keeps your model focused on data, not UI state:

struct Category: Identifiable {
    var id = UUID().uuidString
    var category: categories
}

/// Represent all possible categories cases
enum categories: String, CaseIterable {
    case love = "❤️"
    case family = "👨‍👩‍👦‍👦"
    case urgent = "🆘"
    case sex = "🔥"
    case funny = "😂"
    case school = "🤓"
    case sport = "🏅"
}

#if DEBUG
let categoryTest = [
    Category(category: .love),
    Category(category: .family),
    Category(category: .urgent),
    Category(category: .sex),
    Category(category: .funny),
    Category(category: .school),
    Category(category: .sport)
]
#endif

2. Update the Parent ViewModel (CategoriesViewModel)

Add a property to track the selected category's ID, and a method to handle selection. This becomes your single source of truth for selection state:

import SwiftUI
import Foundation
import Combine

class CategoriesViewModel: ObservableObject {
    @Published var categoryViewModels = [CategoryViewModel]()
    @Published var selectedCategoryId: String? // Tracks the single selected category
    
    private var cancellables = Set<AnyCancellable>()

    init() {
        self.categoryViewModels = categoryTest.map { category in
            CategoryViewModel(category: category)
        }
    }
    
    // Handle category selection: toggle if already selected, or set new selection
    func selectCategory(_ categoryId: String) {
        selectedCategoryId = selectedCategoryId == categoryId ? nil : categoryId
        // If you don't want to allow deselection, use this instead:
        // selectedCategoryId = categoryId
    }
}

3. Simplify the Child ViewModel (CategoryViewModel)

Since we're no longer tracking isActive on the model, this view model can be stripped down to just wrap the Category data:

import SwiftUI
import Foundation
import Combine

class CategoryViewModel: ObservableObject, Identifiable {
    @Published var category: Category
    var id: String { category.id } // Use the category's ID directly

    init(category: Category) {
        self.category = category
    }
}

4. Update the Views

Modify CategoriesView to pass the selection state down to each CategoryView, and handle tap events through the parent view model:

import SwiftUI
import Foundation
import Combine

struct CategoriesView: View {
    @StateObject var categoriesVM = CategoriesViewModel() // Use StateObject for view models initialized here
    var body: some View {
        VStack(alignment: .leading) {
            Text("Catégories")
                .font(.title).bold()
                .offset(y: 20)
            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    ForEach(categoriesVM.categoryViewModels) { categoryVM in
                        CategoryView(
                            categoryVM: categoryVM,
                            isActive: categoriesVM.selectedCategoryId == categoryVM.id,
                            onTap: {
                                categoriesVM.selectCategory(categoryVM.id)
                            }
                        )
                    }
                }
            }
        }
        .padding(.leading, 15)
    }
}

struct CategoriesView_Previews: PreviewProvider {
    static var previews: some View {
        CategoriesView()
    }
}

struct CategoryView: View {
    @ObservedObject var categoryVM: CategoryViewModel
    let isActive: Bool // Receive active state from parent
    let onTap: () -> Void // Receive tap action from parent
    
    var body: some View {
        Text(categoryVM.category.category.rawValue)
            .font(.system(size: isActive ? 26 : 22, weight: .bold))
            .frame(
                width: isActive ? 80 : 60,
                height: isActive ? 80 : 60
            )
            .background(Color.white)
            .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
            .shadow(color: Color.black.opacity(0.1), radius: 10, x: 10, y: 10)
            .onTapGesture(perform: onTap)
            .frame(height: 130)
    }
}

Why This Works

  • Single Source of Truth: The selectedCategoryId in CategoriesViewModel ensures only one category can be selected at a time.
  • Separation of Concerns: Your Category model now only holds data, while view state is managed in the view model—this aligns with MVVM best practices.
  • Reusable Views: CategoryView is now a pure presentation component that just renders based on the state passed to it, making it easier to reuse elsewhere.

If you really want to keep the isActive property on the Category model (not recommended), you could adjust the selectCategory method to loop through all categories and set isActive to false except for the selected one. But this mixes data and state, which can lead to harder-to-maintain code over time.

内容的提问来源于stack exchange,提问作者theovogg

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 19:02:49