iosswiftuicore-graphicsxcode16

SwiftUI: Shape using bezier curve cannot fill with gradient


Thanks for the help, this happened after I updated my Xcode to 16.0 today.

struct TestGradientPath: View {
    @State private var startPoint: CGPoint = .zero
    @State private var endPoint: CGPoint = .init(x: 0, y: 100)
    @State private var controlPoint: CGPoint = .init(x: 100, y: 0)
        
    var body: some View {
        ClosedCurve(startPoint: startPoint,
                         endPoint: endPoint,
                         controlPoint: controlPoint)
        .stroke(.red, lineWidth: 2)
        .fill(
            
            // nope
            
//            LinearGradient(
//                gradient: Gradient(colors: [.red, .blue]),
//                startPoint: .topLeading,
//                endPoint: .bottomTrailing
//            )
            
            // nope
            
            RadialGradient(gradient: Gradient(colors: [.red.opacity(0.3), .red.opacity(0)]),
                                  center: .center,
                                  startRadius: 0,
                                  endRadius: 64)
            
            // works ok
//            .red
        )
    }
}

struct ClosedCurve: Shape {
    var startPoint: CGPoint
    var endPoint: CGPoint
    var controlPoint: CGPoint
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: startPoint)
            path.addQuadCurve(to: endPoint, control: controlPoint)
            path.closeSubpath()
        }
    }
}

#Preview {
    TestGradientPath()
}

Tried several scenarios:


Solution

  • Note the rect parameter of the Shape.path(in:) method. This is the rectangle in which you should draw your shape.

    But you ignored that parameter and just drew a shape in a 100x100 square in the top left corner.

    When you use the shape in the root view, it will try to fill the entire screen, passing the entire screen's rectangle (minus safe area insets) to path(in:). This rectangle is also what it uses to decide where the start point and end point (or the centre point, in case of a radial gradient) of the gradient is.

    In other words, when you say startPoint: .topLeading and endPoint: .bottomTrailing, that means the gradient starts at the very top left of the screen, and ends at the very bottom right of the screen. Since you didn't draw anything on the bottom right, the blue part is not visible at all.

    Similarly for the radial gradient, the centre point of the gradient is at the centre of the screen, and your shape covers a very small angle of the radial gradient, so you only see the transparent part.

    I imagine what you are after is something like this:

    ClosedCurve(startPoint: startPoint,
                     endPoint: endPoint,
                     controlPoint: controlPoint)
    .stroke(.red, lineWidth: 2)
    .fill(
        LinearGradient(
            gradient: Gradient(colors: [.red, .blue]),
            startPoint: .topLeading,
            endPoint: .center
        )
    )
    .frame(width: 100, height: 100)
    // if you still want the shape to be at the top left of the screen, do:
    // .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
    

    Here I have made the shape fill a 100x100 square using frame. The gradient will use this 100x100 square to determine where the start and end points are. I have also changed the end point to .center (of the 100x100 square), because I think it looks better that way

    Here is the result (the green border shows where the 100x100 square is):

    enter image description here

    That said, if you are writing a Shape, you should take the rect parameter of path(in:) into account, and properly draw a shape that fits that rect. You most likely needs to change your current design.