Consider the following code that draws a grid of randomly colored rectangles on the screen, with a Circle that moves to right by responding to a timer:
struct ContentView: View {
let width: CGFloat = 400
let height: CGFloat = 400
let spacing: CGFloat = 8.0
let numRows = 20
var tileSize: CGFloat {
height / 5
}
let timer = Timer.publish(every: 3.0, on: .main, in: .common).autoconnect()
@State private var circleX: CGFloat = 0.0
var body: some View {
let numColumns = Int(tileSize)
ZStack {
Grid(horizontalSpacing: spacing, verticalSpacing: spacing) {
ForEach(0..<numRows, id: \.self) { rowIndex in
GridRow {
ForEach(0..<numColumns, id: \.self) { colIndex in
Rectangle()
.fill(Color.random)
.frame(width: tileSize, height: tileSize)
}
}
}
}
Circle()
.fill(.red)
.frame(width:50, height: 50)
.offset(x: circleX, y: 0)
}
.onReceive(timer) { time in
circleX += 10
}
}
}
#Preview {
ContentView()
}
extension Color {
static var random: Color {
Color(red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1))
}
}
The problem is, I don't want the rectangles to change color. I want them to get a random color to start with and stay that way. What currently happens is that every time the timer is fired, the rectangles all get a new color. The whole view is being re-rendered even though I'm only changing a property that applies to the circle. I've tried putting the .onReceive
modifier on the Circle itself but that doesn't help.
Extract a separate view for the randomly-coloured rectangle.
struct RandomlyColoredRectangle: View {
var body: some View {
Rectangle()
.fill(Color.random)
}
}
This works because the Color.random
call is in another body
. When SwiftUI calls ContentView.body
, RandomlyColoredRectangle.body
is not called because no dependencies of RandomlyColoredRectangle
has changed (in fact it has no dependencies).
The more general way to prevent view updates is to conform to Equatable
:
struct RandomlyColoredRectangle: View, Equatable {
// since this struct has no properties, the automatically generated '=='
// always returns true
var body: some View {
Rectangle()
.fill(Color.random)
}
}
and use RandomlyColoredRectangle
like this:
RandomlyColoredRectangle().equatable()
equatable()
makes it so that the view only updates when the new RandomlyColoredRectangle
is unequal to the old RandomlyColoredRectangle
. Since our ==
always return true, the view never updates.
This is more flexible as you can implement ==
in your own way, so that the view only updates when you want it to.