如何强制更新由ForEach基于Int创建的SwiftUI网格?
问题描述
我制作了一个由空白色块组成的“棋盘”,要求每秒为其中一个方块填充随机颜色。我已为网格填充空白颜色,每秒应为其中一个方块分配新颜色并重新绘制网格,但不清楚如何让SwiftUI执行此更新。
原代码
struct AnimationObject: Hashable, Equatable { var hashValue: Int { return self.placement.0.hashValue + self.placement.1.hashValue } static func == (lhs: AnimationObject, rhs: AnimationObject) -> Bool { return lhs.placement == rhs.placement } var color: Color let placement: (Int, Int) static func emptyArray(rows: Int, colums: Int) -> [AnimationObject] { var emptyArray = [AnimationObject]() for i in 0...rows { for k in 0...colums { emptyArray.append(.init(color: .clear, placement: (i,k))) } } return emptyArray } } struct AnimatedCanvas: View { @State var colors = AnimationObject.emptyArray(rows: 4, colums: 4) let size = CGSize(width: 30, height: 30) let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { Grid(horizontalSpacing: 0, verticalSpacing: 0) { ForEach(0..<4) { i in ForEach(0..<4) { j in let color = colors.filter { $0.placement.0 == i && $0.placement.1 == j}.first! Rectangle() .fill(color.color) .frame(width: 30, height: 30) } }.padding(0) }.onReceive(timer) { input in let i = Int.random(in: 0..<4) let j = Int.random(in: 0..<4) var color = colors.filter { $0.placement.0 == i && $0.placement.1 == j}.first! color.color = Color.random() if colors.count > 30 { colors.removeAll() } } } }
问题原因及修复方案
核心问题
- 数组元素修改无效:
AnimationObject是值类型,在onReceive中你复制了数组里的元素并修改,但这个修改不会同步回原数组,SwiftUI监测不到@State数组的变化,所以不会更新视图。 - 数组初始化范围错误:
emptyArray方法中用了0...rows和0...colums,当传入rows=4时,会生成5行5列共25个元素,但视图里只渲染4x4=16个方块,导致元素数量不匹配。 - 无效的数组清空逻辑:初始化后的数组只有16个元素,
colors.count >30永远为false,这段代码不会执行。
修复后的代码
import SwiftUI // 实现Color的随机扩展 extension Color { static func random() -> Color { Color( red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1) ) } } struct AnimationObject: Hashable, Equatable { var color: Color let placement: (Int, Int) static func == (lhs: AnimationObject, rhs: AnimationObject) -> Bool { return lhs.placement == rhs.placement } static func emptyArray(rows: Int, columns: Int) -> [AnimationObject] { var emptyArray = [AnimationObject]() // 用半开范围..<,确保生成rows行columns列的元素 for i in 0..<rows { for k in 0..<columns { emptyArray.append(.init(color: .clear, placement: (i,k))) } } return emptyArray } } struct AnimatedCanvas: View { @State var colors = AnimationObject.emptyArray(rows: 4, columns: 4) let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { Grid(horizontalSpacing: 0, verticalSpacing: 0) { ForEach(0..<4) { i in GridRow { ForEach(0..<4) { j in // 通过索引直接取元素,比filter更高效 let colorObj = colors[i * 4 + j] Rectangle() .fill(colorObj.color) .frame(width: 30, height: 30) } } } } .onReceive(timer) { _ in let row = Int.random(in: 0..<4) let col = Int.random(in: 0..<4) // 计算元素在数组中的索引,直接修改原数组的元素 let index = row * 4 + col colors[index].color = Color.random() // 优化重置逻辑:当所有方块都被填充颜色时,重置为全透明数组 let filledCount = colors.filter { $0.color != .clear }.count if filledCount >= 16 { colors = AnimationObject.emptyArray(rows: 4, columns: 4) } } } }
关键修改点
- 直接通过索引修改
@State数组中的元素,确保SwiftUI能监测到数组变化并更新视图。 - 修正数组初始化的循环范围,让元素数量和视图渲染的方块数量一致。
- 替换低效的
filter查找,改用索引直接访问元素,提升性能。 - 优化重置逻辑:当所有方块都被填充颜色时,重置为全透明数组。
- 补充了
Color.random()的实现,确保代码可运行。
内容的提问来源于stack exchange,提问作者bogen
相关产品推荐
相关产品推荐

