求助:SwiftUI中Toggle切换时出现闪烁问题的解决方法
问题
修改Toggle的active状态时,所有Toggle会快速闪烁消失后重新显示,现象如下:

核心代码片段:
struct Film: Hashable, Codable, Identifiable { var id = UUID().uuidString let brand: String let stock: String let reciprocity: Double var active = true } private var filmsGroupedByBrand: [GroupedFilm] { return Dictionary(grouping: config.films, by: { $0.brand }) .map { (brand, films) in GroupedFilm(brand: brand, films: films) } .sorted(by: { $0.brand < $1.brand }) .filter( { $0.brand != "Custom" && $0.brand != "Empty" }) } ForEach(filmsGroupedByBrand, id: \.self) { group in Section(group.brand) { ForEach(group.films) { film in if let index = config.films.firstIndex(where: { $0.id == film.id }) { Toggle(isOn: $config.films[index].active) { Text(config.films[index].stock) } } } } }
完整代码:
// // ContentView.swift // tesra Watch App // // Created by Joe Scotto on 10/11/24. // import SwiftUI struct Film: Hashable, Codable, Identifiable { var id = UUID().uuidString let brand: String let stock: String let reciprocity: Double var active = true } struct Defaults { static let films = [ Film(brand: "Ilford", stock: "Ilford Delta 100", reciprocity: 1.26), Film(brand: "Ilford", stock: "Ilford Delta 400", reciprocity: 1.41), Film(brand: "Ilford", stock: "Ilford Delta 3200", reciprocity: 1.33), Film(brand: "Ilford", stock: "Ilford FP4 Plus", reciprocity: 1.26), Film(brand: "Ilford", stock: "Ilford HP5 Plus", reciprocity: 1.31), Film(brand: "Ilford", stock: "Ilford Ortho Plus", reciprocity: 1.25), Film(brand: "Ilford", stock: "Ilford Pan F Plus", reciprocity: 1.33), Film(brand: "Ilford", stock: "Ilford SFX 200", reciprocity: 1.43), Film(brand: "Ilford", stock: "Ilford XP2 Super", reciprocity: 1.31), Film(brand: "Kentmere", stock: "Kentmere Pan 100", reciprocity: 1.26), Film(brand: "Kentmere", stock: "Kentmere Pan 400", reciprocity: 1.30), Film(brand: "Kodak", stock: "Kodak Portra 160", reciprocity: 1.35), Film(brand: "Kodak", stock: "Kodak Portra 400", reciprocity: 1.35), Film(brand: "Empty", stock: "No films enabled", reciprocity: 1.00, active: false), Film(brand: "Custom", stock: "Custom film", reciprocity: 1.3, active: false) ] } class Config: ObservableObject { @Published var films = Defaults.films } struct GroupedFilm: Hashable { let brand: String let films: [Film] } struct ContentView: View { @EnvironmentObject var config: Config private var filmsGroupedByBrand: [GroupedFilm] { return Dictionary(grouping: self.config.films, by: { $0.brand }) .map { (brand, films) in GroupedFilm(brand: brand, films: films) } .sorted(by: { $0.brand < $1.brand }) .filter( { $0.brand != "Custom" && $0.brand != "Empty" }) } var body: some View { VStack { List { ForEach(filmsGroupedByBrand, id: \.self) { group in Section(group.brand) { ForEach(group.films, id: \.id) { film in if let index = config.films.firstIndex(where: { $0.id == film.id }) { Toggle(isOn: $config.films[index].active) { Text(config.films[index].stock) } } } } } } } } } #Preview { ContentView() .environmentObject(Config()) }
解决方案
问题根源
GroupedFilm哈希逻辑错误:默认的Hashable实现会把films数组纳入哈希计算,当任意Film的active属性变化时,整个分组的哈希值改变,外层ForEach会认为所有分组都是新元素,触发全量视图重建,导致闪烁。- 冗余的索引查找:通过
firstIndex定位影片的方式,让SwiftUI无法精准追踪单个视图的状态,进一步扩大了重建范围。
修改步骤
- 锁定
GroupedFilm的哈希标识:手动实现Hashable,仅用brand作为分组的唯一标识,这样分组内影片属性变化时,分组本身的标识不会改变,外层ForEach不会重建整个Section。 - 直接绑定单个影片:使用
ForEach($config.films)拿到影片的绑定对象,直接关联Toggle的状态,避免索引查找,让SwiftUI精准更新单个视图。
修改后的代码
struct GroupedFilm: Hashable { let brand: String let films: [Film] // 仅基于brand判断相等性和计算哈希 static func == (lhs: GroupedFilm, rhs: GroupedFilm) -> Bool { lhs.brand == rhs.brand } func hash(into hasher: inout Hasher) { hasher.combine(brand) } } struct ContentView: View { @EnvironmentObject var config: Config private var filmsGroupedByBrand: [GroupedFilm] { return Dictionary(grouping: self.config.films, by: { $0.brand }) .map { (brand, films) in GroupedFilm(brand: brand, films: films) } .sorted(by: { $0.brand < $1.brand }) .filter( { $0.brand != "Custom" && $0.brand != "Empty" }) } var body: some View { VStack { List { ForEach(filmsGroupedByBrand, id: \.self) { group in Section(group.brand) { ForEach($config.films) { $film in // 仅显示当前分组下的影片 if film.brand == group.brand { Toggle(isOn: $film.active) { Text(film.stock) } } } } } } } } }
可选优化
如果想进一步提升性能,可以把分组逻辑迁移到Config类中,作为@Published的计算属性,减少视图层的重复计算,但上述修改已经足够解决闪烁问题。
内容的提问来源于stack exchange,提问作者Joe Scotto
相关产品推荐
相关产品推荐

