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

求助: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())
}

解决方案

问题根源

  1. GroupedFilm哈希逻辑错误:默认的Hashable实现会把films数组纳入哈希计算,当任意Film的active属性变化时,整个分组的哈希值改变,外层ForEach会认为所有分组都是新元素,触发全量视图重建,导致闪烁。
  2. 冗余的索引查找:通过firstIndex定位影片的方式,让SwiftUI无法精准追踪单个视图的状态,进一步扩大了重建范围。

修改步骤

  1. 锁定GroupedFilm的哈希标识:手动实现Hashable,仅用brand作为分组的唯一标识,这样分组内影片属性变化时,分组本身的标识不会改变,外层ForEach不会重建整个Section。
  2. 直接绑定单个影片:使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 12:10:59