I'm pretty sure this is a pretty simple problem and it's root is in the ForEach(group.films)
section having to redraw whenever $config
is changed. The issue is that when I change the Toggle
value, they all flash quickly off then back on depending on if they're .active
or not:
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)
}
}
}
}
}
Full code:
//
// 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())
}
The problem is that filmsGroupedByBrand
is a computed property and it makes use of the published property films
in Config
so every time film
changes the computed property is recalculated and you get the flashing behaviour in the view.
But if we look at the code in the computed property it is really a static property or at least not updated by any changes in the view so I moved it to the Config
class instead and changed it into a stored property
class Config: ObservableObject {
@Published var films = Defaults.films
let filmsGroupedByBrand: [GroupedFilm]
init() {
filmsGroupedByBrand = Dictionary(grouping: Defaults.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" }
}
}
Now that this is not a computed property it will not be recalculated for each change and the view will behave as expected without any flashing toggles.
An alternative is of course to instead have the stored property in the view and as above set its value in the init, depending on how your real code looks one might be better than the other.