2

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) }
        
        
    }
}

1 Answer 1

2

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:

  1. 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

  2. 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.

Sign up to request clarification or add additional context in comments.

16 Comments

thanks for your answer, but I am really confused, I cannot understand and follow your way, and what is happening, also not big fan of using Binding.init()
I just accept the answer because solve the issue, but I really did not understand the answer. thanks for your time.
Equatable is very applicable here because the system uses it to determine whether or not to re-render the element. It's the key to a lot of SwiftUI performance.
@swiftPunk ForEach will use the == to see when to reprender view
Well, I'm not confident that I understand exactly when and how SwiftUI uses equatable. Here's a good resource: swiftui-lab.com/equatableview
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.