Search code examples
swiftuiviewpathshapes

SwiftUI - Getting Path to redraw after variables updated


I have a TextField that dictates the angle of a solar panel, the default is 10 degrees.

enter image description here

If I alter this to 80 the variables update as shown below:

enter image description here

What doesn't happen is that the Path drawing the panels doesn't get refreshed. The text clearly does.

displayPanelSpacing is called which in turn calls drawPanels. From the print statement in drawPanels I know that it is receiving the correct, updated numbers.

The grey lines only seem to be generating when the view loads for the first time. What am I doing wrong?

struct displayPanelSpacing: View{
    @ObservedObject var variables: Variables
    @State private var changeLine = false
    @State private var offset: CGFloat = 20.0
    
    var body: some View {
        VStack {
            Text("Panel Spacing " + String(variables.tiltAngle) + "°")
            
            drawPanels(variables: variables)
                .stroke(changeLine ? Color(.red) : .gray, lineWidth: 3.0)
                .frame(width: (GlobalVariables.screenWidth - 64), height: 150)
                .padding(.leading, 16)
                .padding(.trailing, 16)
                .scaleEffect(changeLine ? 0.5 : 1.0)
                .overlay(DrawSun(radius: 25).foregroundColor(.orange))
            // .animation(Animation.linear(duration: 1.0), value: offset)
        }
    }
}

struct drawPanels: Shape {
    @ObservedObject var variables: Variables

    func path(in rect: CGRect) -> Path {
        let result = variables.ed()
        let edEndX = result.endX
        let edEndY = result.endY

        let baselineY = rect.maxY - 0
        let baselineXOffset = 10
        let panelY = Int(rect.maxY) - edEndY
        let panel1X = baselineXOffset + edEndX
        let panel2X = baselineXOffset + Int(rect.midX) + edEndX
        
        var path = Path()
        path.move(to: CGPoint(x: rect.minX + 10, y: baselineY))
        path.addLine(to: CGPoint(x: panel1X, y: panelY))
        path.move(to: CGPoint(x: rect.midX + 10, y: baselineY))
        path.addLine(to: CGPoint(x: panel2X, y: panelY))
        let _ = print("Received", String(edEndY), String(panelY))
        return path
    }
}

The input TextField is:

struct PanelSpacingView: View {
    @StateObject var variables: Variables = Variables()
    ...
    var body: some View {
    ...
      HStack {
         Text("Tilt Angle")
              .padding(.leading, 16)
         TextField("Tilt Angle", value: $variables.tiltAngle, formatter: formatter)
              .textFieldStyle(.roundedBorder)
              .disableAutocorrection(true)
              .padding(.leading, 16)
              .padding(.trailing, 16)                          
         Text("°")
               .padding(.trailing, 16)
       }

Solution

  • Although the Shape protocol includes View it seems it does not work with the @ObservedObject var variables: Variables in the DrawPanels.

    So I suggest you use a simpler approach, using a @Binding in DrawPanels as shown in the example code.

    Without a complete workable code, all I can do, is propose an approach that should work. Adjust the code to your specific needs.

    struct DisplayPanelSpacing: View {
        @ObservedObject var variables: Variables
        @State private var changeLine = false
        @State private var offset: CGFloat = 20.0
        
        var body: some View {
            VStack {
                Text("Panel Spacing " + String(variables.tiltAngle) + "°")
                
                DrawPanels(angle: $variables.tiltAngle)  // <-- here, pass whatever is needed
                    .stroke(changeLine ? .red : .gray, lineWidth: 3.0)
                    .frame(width: (GlobalVariables.screenWidth - 64), height: 150)
                    .padding(.leading, 16)
                    .padding(.trailing, 16)
                    .scaleEffect(changeLine ? 0.5 : 1.0)
                    .overlay(DrawSun(radius: 25).foregroundColor(.orange))
                    .animation(Animation.linear(duration: 1.0), value: offset)
            }
        }
    }
    
    struct DrawPanels: Shape {
        @Binding var angle: Int  // <-- here
        
        func path(in rect: CGRect) -> Path {
            let result = Result(endX: angle, endY: 33)  // <-- here, whatever the equivalent of `variables.ed()`
            let edEndX = result.endX
            let edEndY = result.endY
            
            let baselineY = rect.maxY - 0
            let baselineXOffset = 10
            let panelY = Int(rect.maxY) - edEndY
            let panel1X = baselineXOffset + edEndX
            let panel2X = baselineXOffset + Int(rect.midX) + edEndX
            
            var path = Path()
            path.move(to: CGPoint(x: rect.minX + 10, y: baselineY))
            path.addLine(to: CGPoint(x: panel1X, y: panelY))
            path.move(to: CGPoint(x: rect.midX + 10, y: baselineY))
            path.addLine(to: CGPoint(x: panel2X, y: panelY))
            return path
        }
    }
    
    // for testing
    struct ContentView: View {
        @StateObject var variables: Variables = Variables()
        
        var body: some View {
            VStack {
                HStack {
                    Text("Tilt Angle").padding(.leading, 16)
                    TextField("Tilt Angle", value: $variables.tiltAngle, formatter: NumberFormatter())
                        .textFieldStyle(.roundedBorder)
                        .disableAutocorrection(true)
                        .padding(.leading, 16)
                        .padding(.trailing, 16)
                    Text("°").padding(.trailing, 16)
                }
                DisplayPanelSpacing(variables: variables)
            }
        }
        
    }
    
    // for testing
    class Variables: ObservableObject {
        @Published var tiltAngle = 10
    }
    // for testing
    struct Result {
        var endX: Int
        var endY: Int
    }