Search code examples
iossvgswiftuirenderinglinear-gradients

SwiftUI: stretch LinearGradient on a rectangle


I am trying to render a linear gradient on a rectangle. Here is the code:

Rectangle()
        .foregroundColor(Color.red)
        .overlay(
            LinearGradient(gradient: Gradient(colors: [.red, .yellow, .blue]), startPoint: .topLeading, endPoint: .bottomTrailing)
        )
        .frame(width: 300, height: 200)
        .position(x: 170, y: 120)

When I render it on square, everything looks correct:

square is great

When I render it on a rectangle, however, it stops looking like it's going from topLeading corner to bottomTrailing. It just looks like it's the same one which was clipped:

enter image description here

Here is how it's rendered in svg (blue is not the same it seems, but that's not the important part), and how I want it to look in swift:

enter image description here

The yellow diagonal should go directly from one corner to another, since I am specifying startPoint: .topLeading, endPoint: .bottomTrailing. It says here SwiftUI diagonal LinearGradient in a rectangle that this is a standard behaviour, and that's ok - I'm not trying to say SwiftUI renders it incorrectly, I just need a way to make it look like in svg.


Solution

  • What a fun problem :)

    The "naive" approach would be to render the gradient as a square and then either rotate it or squeeze or stretch it to fit the rectangle. Rotating requires more maths so here's the "squeezing" version:

    struct ContentView: View {
        var body: some View {
            
            Rectangle()
                .overlay(
                    GeometryReader { g in
                        LinearGradient(
                            gradient: Gradient(colors: [.red, .yellow, .blue]),
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        )
                        .frame(width: g.size.width, height: g.size.width)
                        .scaleEffect(x: 1.0, y: g.size.height / g.size.width, anchor: .top)
                    }
                )
                .frame(width: 300, height: 200)
            
        }
    }
    

    Notes:

    • To do the squeeze, we need to know the proportions of the rectangle, you were already using an overlay() and overlays are bounded by the Rectangle() in this case, so we can just read the size with the GeometryReader();
    • We need to start with the gradient applied to a square and there is a couple of ways to do squares. As we already know the size of the rectangle, I went with a square with a side equal to the width of the enclosing rectangle;
    • Then we apply the scaleEffect(), squeezing the height of the square to the height of the rectangle.

    Please let me know if that's what you were looking for or if there is a better way to do to this!

    Cheers, –Baglan