Binding with a ternary between parent/child views in SwiftUI

75 Views Asked by At

Consider this code in which I'm trying to continuously animate a set of circles from red to blue:

struct ContentView: View {
    let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    @State var colorOn = false
    
    var body: some View {
        VStack {
        ForEach(0...10, id: \.self) { n in
            CircleView(size: 30, isOn: n%2 == 0 ? $colorOn : !$colorOn)
                .onReceive(timer) { _ in
                    colorOn.toggle()
                }
        }
    }
    }
}

#Preview {
    ContentView()
}

struct CircleView: View {
    var size: CGFloat
    @Binding var isOn: Bool

    var body: some View {
        ZStack {
            Circle()
                .fill(isOn ? .red : .blue)
                .frame(width: size, height: size)
                .animation(.easeInOut(duration: 0.5), value: isOn)
            
        }
    }
}

The trick is that I need them to alternate colors, as in every other circle should be red while the ones in between are blue. But I can't get the syntax right on the @Bindable property:

Cannot convert value '$colorOn' of type 'Binding<Bool>' to expected type 'Bool', use wrapped value instead

1

There are 1 best solutions below

3
son On BEST ANSWER

A @Binding property means that the value actually comes from somewhere else and can be shared with other places. It create a two way binding between the view (CircleView), and the stored data (in this case is @State var colorOn in ContentView).

It was correct if inside CircleView, you also had an action that could modify isOn such as:

struct CircleView: View {
    @Binding var isOn: Bool

    var body: some View {
        ZStack {
            Circle()
                ...
                .onTapGeture {
                    //Stop animation immediately
                    //It will impact to `colorOn` in `ContentView`
                    isOn = false
                }
        }
    }
}

However, your purpose is to swap colors every single second, depending on the timer. So, you have nothing to do with @Binding since the CircleView is used only for displaying data. You just need to define a Bool that represents its on/off states.

struct CircleView: View {
    var size: CGFloat
    var isOn: Bool //<- Remove binding here

    var body: some View {
        ZStack {
            Circle()
                .fill(isOn ? .red : .blue)
                .frame(width: size, height: size)
                .animation(.easeInOut(duration: 0.5), value: isOn)
        }
    }
}

The last step is to remove Binding ($) from ContentView then you're good to go.

struct ContentView: View {
    @State private var colorOn = false
    var body: some View {
        ...
        CircleView(size: 30, isOn: n % 2 == 0 ? colorOn : !colorOn)
        ...
    }
}

Output

enter image description here