swiftPunk
swiftPunk

Reputation: 1

How can I update a ForEach without unnecessary rendering all array of ForEach, working with Binding Element in SwiftUI?

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

Answers (1)

jnpdx
jnpdx

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:

  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.

Upvotes: 2

Related Questions