jpelayo
jpelayo

Reputation: 413

Custom view won't use state variable update provided through binding, but debug watch shows changes

I'm starting to understand the @Binding and @State ways of the SwiftUI. Or at least I like to think so. Anyway, there is some debugging results that puzzle me a lot. Let me explain.

The objective here is to control the position of a "floating" view on the ContentView. This involves a message sending a @binding variable to a @State in the ContentView. This works: you can see it in the debugger. The expected result is a floating rectangle that changes position in the screen when the gear button is pushed.

The floating view can be passed its own @State to control 'where' it floats (high or low in the 'y' coordinate). This works if the ViewPosition is passed hardcoded.

Now, the problem is that the puzzle works well if you watch the values being passed in the debugger, but the fact is that the floatong view always uses the same value to work with. How can this be?

We can see the effects in the attached code, setting breakpoints in the lines 120 and 133 if the alternative case is watched or 76 if the default case is being watched.

Code is cut-paste in a new project for a tabbed swiftui app.

I tried both coarse ways that are presented for two different ContentView options (rename to change the execution branch). It is important to watch the variables in the debugger in order to enjoy the full puzzlement experience as .high and .low values are being well passed, but the rectangle remains still.

//
//  ContentView.swift
//  TestAppUno
//


import SwiftUI


struct MenuButton1: View {
    @Binding var menuButtonAction: Bool
    var title: String = "--"
    var body: some View {
        Button(action: {self.menuButtonAction.toggle()}) {
            Image(systemName:"gear")
                .resizable()
                .imageScale(.large)
                .aspectRatio(1, contentMode: .fit)
                .frame(minWidth: 50, maxWidth: 50, minHeight: 50, maxHeight: 50, alignment: .topLeading)

            }
            .background(Color.white.opacity(0))
            .cornerRadius(5)
            .padding(.vertical, 10)
            .position(x: 30, y: 95)
    }

}

struct MenuButton2: View {
    @Binding var menuButtonAction: ViewPosition
    var title: String = "--"
    var body: some View {
        Button(action: {self.toggler()}) {
            Image(systemName:"gear")
                .resizable()
                .imageScale(.large)
                .aspectRatio(1, contentMode: .fit)
                .frame(minWidth: 50, maxWidth: 50, minHeight: 50, maxHeight: 50, alignment: .topLeading)

            }
            .background(Color.white.opacity(0))
            .cornerRadius(5)
            //.border(Color.black, width: 1)
            .padding(.vertical, 10)
            .position(x: 30, y: 95)

    }

    func toggler()->ViewPosition {
        if (self.menuButtonAction == ViewPosition.high) { self.menuButtonAction = ViewPosition.low; return ViewPosition.low } else { self.menuButtonAction = ViewPosition.high; return ViewPosition.low }
    }

}

struct ContentView: View {

    @State private var selection = 0
    @State var moveCard = false
    @State var vpos = ViewPosition.low


    var body: some View {

        TabbedView(selection: $selection){

            ZStack() {


                MenuButton2(menuButtonAction: $vpos)

                //if(self.moveCard){self.vpos = ViewPosition.low} else {self.vpos = ViewPosition.low }
                    // Correct answer, change 1 of 3
                    //TestView(aposition: $vpos) {    // <-- OK
                    TestView(aposition:self.vpos) {

                        VStack(alignment: HorizontalAlignment.center, spacing: 1.0){

                            Text("See here")
                                .font(.headline)

                            }
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)

                    }


                }
                .tabItemLabel(Image("first"))
                .tag(0)

            Text("Nothing here")
                .tabItemLabel(Image("second"))
                .tag(1)
        }


    }
}

                    // Correct answer, change 2 of 3
struct ContentView1: View { // <-- Remove this block 

    @State private var selection = 0
    @State var moveCard = false
    @State var cardpos = ViewPosition.low


    var body: some View {

        TabbedView(selection: $selection){

            ZStack() {

                MenuButton1(menuButtonAction: $moveCard)

                if(self.moveCard){

                    TestView(aposition:ViewPosition.low) {

                    VStack(alignment: HorizontalAlignment.center, spacing: 1.0){

                        Text("See here")
                            .font(.headline)

                        }
                        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)

                }
                }else{

                    TestView(aposition:ViewPosition.high) {

                        VStack(alignment: HorizontalAlignment.center, spacing: 1.0){

                            Text("See here")
                                .font(.headline)

                            }
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)

                    }
                }

                }
                .tabItemLabel(Image("first"))
                .tag(0)

            Text("Nothing here")
                .tabItemLabel(Image("second"))
                .tag(1)
        }


    }
}



struct TestView<Content: View> : View {
    @State var aposition : ViewPosition
    //proposed solution #1
    //var aposition : ViewPosition
    //proposed solution #2 -> Correct
    //@Binding var aposition : ViewPosition // <- Correct answer, change 3 of 3

    var content: () -> Content
    var body: some View {

        print("Position: " + String( format: "%.3f", Double(self.aposition.rawValue)))


        return Group {
            self.content()
            }
            .frame(height: UIScreen.main.bounds.height/2)
            .frame(width: UIScreen.main.bounds.width)
            .background(Color.red)
            .cornerRadius(10.0)
            .shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
            .offset(y: self.aposition.rawValue )


    }


}



enum ViewPosition: CGFloat {
    case high = 50
    case low = 500
}


#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

No errors, compile well and variables are passed. Hardcoded passed values to the floating view can be made, and the rectangle responds, but not if values are provided programmatically.

Upvotes: 1

Views: 995

Answers (1)

kontiki
kontiki

Reputation: 40599

Just remove the @State in @State var aposition in the TestView. Basically, @State variables are meant to represent sources of truth, so they are never passed a value from a higher View.

I could write a long explanation on how bindings work, but it is perfectly explained in the WWDC session Data Flow with SwiftUI. It only takes 37 minutes and will save you a lot of time eventually. It makes the clear distinction of when you need to use: @State, @Binding, @BindingObject and @EnvironmentObject.

struct TestView<Content: View> : View {
    var aposition : ViewPosition

    var content: () -> Content
    var body: some View {

        print("Position: " + String( format: "%.3f", Double(self.aposition.rawValue)))


        return Group {
            self.content()
        }
        .frame(height: UIScreen.main.bounds.height/2)
            .frame(width: UIScreen.main.bounds.width)
            .background(Color.red)
            .cornerRadius(10.0)
            .shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
            .offset(y: self.aposition.rawValue )
    }
}

Upvotes: 1

Related Questions