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

SwiftUI采用MVVM架构时分类视图点击后不更新问题排查

SwiftUI分类菜单选中状态不更新问题解决

问题描述

我有两个SwiftUI视图CategoriesListView(导航菜单)和CategoryView(可点击项),想要实现:

  • 初始仅第一个分类高亮
  • 点击某个分类时,该分类显示高亮图标
  • 之前选中的分类取消高亮

但点击后视图毫无变化,刚接触SwiftUI,想知道是不是需要在ViewModel里调整?

原视图代码

struct CategoriesListView: View {
    @Environment(CategoriesListViewModel.self) var categoriesListViewModel
    
    var selectedIndex: Int { categoriesListViewModel.categories.firstIndex(where: {
            $0.isSelected == true
        }) ?? 0
    }
    
    var body: some View {
        @Bindable var categoriesListViewModel = categoriesListViewModel
        ScrollView(.horizontal) {
            HStack(alignment: .top, spacing: 12) {
                Spacer()
                ForEach(Array(categoriesListViewModel.categories.enumerated()), id: \.offset) { index, element in
                    CategoryView(categoryViewModel: $categoriesListViewModel.categories[index]).onTapGesture {
                        categoriesListViewModel.categories[selectedIndex].isSelected = false
                        categoriesListViewModel.categories[index].isSelected = true
                        CategoryView(categoryViewModel: $categoriesListViewModel.categories[index])
                    }
                }
            }
        }
    }
}

struct CategoryView: View {
    @Binding var categoryViewModel: CategoryViewModel
    
    var body: some View {
        if categoryViewModel.isSelected {
            categoryViewModel.category.highlightedIconImage
        } else {
            categoryViewModel.category.iconImage
        }
    }
}

原ViewModel代码

@Observable
class CategoriesListViewModel {
    var categories: [CategoryViewModel] = []
    var currentSelection: Int = 0
    var previousSelection: Int? = nil
    
    init(categories: [CategoryViewModel], currentSelection: Int, previousSelection: Int? = nil) {
        self.categories = setCategoriesToShow()
        self.currentSelection = currentSelection
        self.previousSelection = previousSelection
    }
    
    func setCategoriesToShow() -> [CategoryViewModel] {
        var categoriesToShow = [CategoryViewModel]()
        let resortCategory = Category(
            identifier: "a",
            title: "Resort",
            outfits: [],
            iconImage: Image("ResortUnselected"),
            highlightedIconImage: Image("ResortSelected")
        )
        
        var europeCategory = Category(
            identifier: "b",
            title: "Europe",
            outfits: [],
            iconImage: Image("EuropeUnselected"),
            highlightedIconImage: Image("EuropeSelected")
        )
        
        var brunchCategory = Category(
            identifier: "c",
            title: "Brunch",
            outfits: [],
            iconImage: Image("BrunchUnselected"),
            highlightedIconImage: Image("BrunchSelected")
        )
        
        var athleisureCategory = Category(
            identifier: "d",
            title: "Athleisure",
            outfits: [],
            iconImage: Image("AthleisureUnselected"),
            highlightedIconImage: Image("AthleisureSelected")
        )
        
        var workCategory = Category(
            identifier: "e",
            title: "Work",
            outfits: [],
            iconImage: Image("WorkUnselected"),
            highlightedIconImage: Image("WorkSelected")
        )
        
        categoriesToShow.append(CategoryViewModel(category: resortCategory, isSelected: true))
        categoriesToShow.append(CategoryViewModel(category: europeCategory, isSelected: false))
        categoriesToShow.append(CategoryViewModel(category: brunchCategory, isSelected: false))
        categoriesToShow.append(CategoryViewModel(category: athleisureCategory, isSelected: false))
        categoriesToShow.append(CategoryViewModel(category: workCategory, isSelected: false))
        
        return categoriesToShow
    }
}

class CategoryViewModel: ObservableObject, Identifiable {
    var category: Category
    var isSelected: Bool
    
    init(category: Category, isSelected: Bool) {
        self.category = category
        self.isSelected = isSelected
    }
}

问题原因

  1. 状态变更无法触发刷新:CategoryViewModel的isSelected未标记为可观察属性,且CategoriesListViewModel的@Observable仅监听数组本身的增删改,不会响应数组元素内部的属性变化。
  2. 无效视图操作:onTapGesture里创建CategoryView的代码完全无用,只是生成了一个视图实例,并未替换界面上的现有视图。
  3. 状态管理分散:在视图中直接操作数组元素的选中状态,逻辑分散且易出错。

修复方案

1. 调整ViewModel结构

将CategoryViewModel改为@Observable修饰,集中管理选中切换逻辑:

import SwiftUI

// 补充Category结构体的完整定义
struct Category: Identifiable {
    let id = UUID()
    var identifier: String
    var title: String
    var outfits: [Any]
    var iconImage: Image
    var highlightedIconImage: Image
}

@Observable
class CategoriesListViewModel {
    var categories: [CategoryViewModel] = []
    
    init() {
        self.categories = setCategoriesToShow()
    }
    
    func setCategoriesToShow() -> [CategoryViewModel] {
        let resortCategory = Category(
            identifier: "a",
            title: "Resort",
            outfits: [],
            iconImage: Image("ResortUnselected"),
            highlightedIconImage: Image("ResortSelected")
        )
        
        let europeCategory = Category(
            identifier: "b",
            title: "Europe",
            outfits: [],
            iconImage: Image("EuropeUnselected"),
            highlightedIconImage: Image("EuropeSelected")
        )
        
        let brunchCategory = Category(
            identifier: "c",
            title: "Brunch",
            outfits: [],
            iconImage: Image("BrunchUnselected"),
            highlightedIconImage: Image("BrunchSelected")
        )
        
        let athleisureCategory = Category(
            identifier: "d",
            title: "Athleisure",
            outfits: [],
            iconImage: Image("AthleisureUnselected"),
            highlightedIconImage: Image("AthleisureSelected")
        )
        
        let workCategory = Category(
            identifier: "e",
            title: "Work",
            outfits: [],
            iconImage: Image("WorkUnselected"),
            highlightedIconImage: Image("WorkSelected")
        )
        
        return [
            CategoryViewModel(category: resortCategory, isSelected: true),
            CategoryViewModel(category: europeCategory, isSelected: false),
            CategoryViewModel(category: brunchCategory, isSelected: false),
            CategoryViewModel(category: athleisureCategory, isSelected: false),
            CategoryViewModel(category: workCategory, isSelected: false)
        ]
    }
    
    // 集中处理选中切换逻辑
    func selectCategory(at index: Int) {
        categories.indices.forEach { categories[$0].isSelected = false }
        categories[index].isSelected = true
    }
}

@Observable
class CategoryViewModel: Identifiable {
    let id = UUID()
    var category: Category
    var isSelected: Bool
    
    init(category: Category, isSelected: Bool) {
        self.category = category
        self.isSelected = isSelected
    }
}

2. 优化视图代码

移除无效代码,调用ViewModel的统一方法处理选中:

struct CategoriesListView: View {
    @Environment(CategoriesListViewModel.self) var categoriesListViewModel
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack(alignment: .top, spacing: 12) {
                Spacer()
                ForEach($categoriesListViewModel.categories) { $categoryVM in
                    CategoryView(categoryViewModel: $categoryVM)
                        .onTapGesture {
                            if let index = categoriesListViewModel.categories.firstIndex(where: { $0.id == categoryVM.id }) {
                                categoriesListViewModel.selectCategory(at: index)
                            }
                        }
                }
            }
        }
    }
}

struct CategoryView: View {
    @Binding var categoryViewModel: CategoryViewModel
    
    var body: some View {
        categoryViewModel.isSelected ? categoryViewModel.category.highlightedIconImage : categoryViewModel.category.iconImage
    }
}

关键修改说明

  • @Observable修饰CategoryViewModel:让isSelected的变化能被SwiftUI监听,触发视图自动刷新。
  • 集中状态管理:把选中切换逻辑封装到selectCategory方法中,避免视图直接操作数组,逻辑更清晰易维护。
  • 移除无效代码:删掉onTapGesture里创建CategoryView的无用代码,视图更新完全由状态驱动。

内容的提问来源于stack exchange,提问作者jane-bun

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 00:45:54