skip to Main Content

I was playing around with @resultBuilder and made a simple result builder called SumBuilder which takes a bunch of integers and adds them all up.

@resultBuilder
struct SumBuilder {
    static func buildBlock(_ components: Int...) -> Int {
        components.reduce(0, +)
    }
}

Since I was also playing around with shapes the other day I thought about creating a custom shape which can use this new result builder in its implementation. The custom shape just draws a line from the origin of the container to the point specified.

struct CustomShape: Shape {
    @SumBuilder let sum: () -> Int
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: sum(), y: sum()))
        }
    }
}

When I hooked up sum to a @State variable in my ContentView it works perfectly. However, when I change sum using .onAppear(), the value of sum will change under the hood but the View doesn’t update to reflect that:

struct ContentView: View {
    @State private var distance = 0
    var body: some View {
        VStack {
            Button("Increase distance") {
                distance += 100
            }
            
            CustomShape { distance }
                .stroke()
                .onAppear { distance = 100 }  // << Updates distance, but the View still doesn't show
                                              // the line until I press the Button to make distance = 200
        }
    }
}

I tried moving the .onAppear() to the Text thinking the .onAppear() wasn’t being called since the CustomShape is initially a dot, but that didn’t change anything. What’s even stranger is that when I transform the CustomShape to a regular View, the .onAppear() works as intended.

Does anyone know how to make a Shape which utilises @resultBuilder update even when a value is changed using .onAppear()? If you can help in any shape or form I’d be very grateful.

Thanks in advance!

P.S. This is only the least amount of code required to recreate the problem. In my real app I’m initialising a variable called endPoint to be the centre using a GeometryProxy. That’s why I can’t initialise distance directly when I declare it as it would be conceptually different than what I’m actually trying to achieve.

2

Answers


  1. As i mentioned, i just tried your code but using a ViewModel, which worked as expected. In production you probably want to use input-ouput-pattern but this is the least amount of code to explain what i meant by using a ViewModel.

    extension ContentView {
        final class ViewModel: ObservableObject {
            @Published var distance = 0
        }
        
    }
    struct ContentView: View {
        @StateObject var viewModel = ViewModel()
        var body: some View {
            VStack {
                Button("Increase distance") {
                    viewModel.distance += 100
                }
                
                CustomShape { viewModel.distance }
                    .stroke()
                    .onAppear {
                        viewModel.distance = 100
                    }
            }
        }
    }
    
    Login or Signup to reply.
  2. In SwiftUI, the .onAppear() modifier is used to perform some action when a view appears on the screen. However, this modifier does not trigger a re-render of the view, so any changes made within the .onAppear() closure will not be reflected in the view’s appearance.

    This applies to shapes that use @resultBuilder as well. If a shape is created using @resultBuilder and you make changes to it within the .onAppear() closure, the changes will not be visible in the view. To update a shape that uses @resultBuilder you need to use .onChange() or .onReceive() instead of .onAppear() to update the shape which will trigger the view to re-render.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search