Reputation: 1
I have an array of Color, which I am using this array for rendering a View in multiple time, Each element of this array has a State role for a View that works with Binding, So we have an array as array of State, which every single element of that array is going used for feeding a View which needs Binding, the codes working, but every single small update to this array make ForEach render the all array, which is unnecessary, I want to correct or modify my code to stop this unnecessary renders! For example when I change 1 element to Color.black, it is understandable that SwiftUI render a new View for this element but in the fact my codes make SwiftUI render all array! or when I add new element to end of array, the same thing happens! How can I solve this problem? thanks.
PS: if you think that index or id:.self make this issue, I have to say No, because I must and I have to use index, because Binding needs an State object, and it is only possible with index, I cannot use item version of ForEach, because Binding cannot update it.
var randomColor: Color { return Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1)) }
struct BindingWay: View {
@State private var arrayOfColor: [Color] = [Color]()
var body: some View {
VStack(spacing: 0) {
ForEach(arrayOfColor.indices, id:\.self) { index in
CircleViewBindingWay(colorOfCircle: $arrayOfColor[index])
}
Spacer()
Button("append new Color") {
arrayOfColor.append(randomColor)
}
.padding(.bottom)
Button("update last element color to black") {
if arrayOfColor.count > 0 {
arrayOfColor[arrayOfColor.count - 1] = Color.black
}
}
.padding(.bottom)
}
.shadow(radius: 10)
}
}
struct CircleViewBindingWay: View {
@Binding var colorOfCircle: Color
init(colorOfCircle: Binding<Color>) { print("initializing CircleView"); _colorOfCircle = colorOfCircle }
var body: some View {
print("rendering CircleView")
return Circle()
.fill(colorOfCircle)
.frame(width: 50, height: 50, alignment: .center)
.onTapGesture { colorOfCircle = colorOfCircle.opacity(0.5) }
}
}
Upvotes: 2
Views: 777
Reputation: 52367
The following works:
struct CircleViewBindingWay: View {
@Binding var colorOfCircle: Color
init(colorOfCircle: Binding<Color>) { print("initializing CircleView"); _colorOfCircle = colorOfCircle }
var body: some View {
print("rendering CircleView")
return Circle()
.fill(colorOfCircle)
.frame(width: 50, height: 50, alignment: .center)
.onTapGesture { colorOfCircle = colorOfCircle.opacity(0.5) }
}
}
extension CircleViewBindingWay : Equatable { //<-- here
static func == (lhs: CircleViewBindingWay, rhs: CircleViewBindingWay) -> Bool {
lhs.colorOfCircle == rhs.colorOfCircle
}
}
struct ContentView: View {
@State private var arrayOfColor: [Color] = [Color]()
var randomColor: Color { return Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1)) }
var body: some View {
VStack(spacing: 0) {
ForEach(arrayOfColor.indices, id: \.self) { index in
CircleViewBindingWay(colorOfCircle: .init(get: { () -> Color in //<-- here
arrayOfColor[index]
}, set: { (newValue) in
arrayOfColor[index] = newValue
}))
}
Spacer()
Button("append new Color") {
arrayOfColor.append(randomColor)
}
.padding(.bottom)
Button("update last element color to black") {
if arrayOfColor.count > 0 {
arrayOfColor[arrayOfColor.count - 1] = Color.black
}
}
.padding(.bottom)
}
.shadow(radius: 10)
}
}
What has to happen:
CircleViewBindingWay
conforms to Equatable
and checks that the colors are the same. ForEach
does the equatable check itself, which is why actually attaching .equatable()
isn't necessary
The Binding
is declared inline. There must be another equatable check that ForEach
/SwiftUI
does on the $arrayOfColor
that fails, but this inline one passes.
Upvotes: 2