swiftswiftuiproperty-wrapper

Binding with a ternary between parent/child views in SwiftUI


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


Solution

  • 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