iosswiftuikitlinear-gradientscagradientlayer

iOS drawLinearGradient diagonal not working correctly when view is longer than width


I can demonstrate the issue with this simple example.

I have a view which needs diagonal gradient which should start with one color at top left corner and change to another color at the bottom right corner. The following code shows my attempt:

import UIKit

class GradientView: UIView {

    override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()!
        context.saveGState()
        context.clip(to: rect)
        
        let topColor = UIColor.red
        let bottomColor = UIColor.green
        
        var tr = CGFloat(0), tg = CGFloat(0), tb = CGFloat(0), ta = CGFloat(0), br = CGFloat(0), bg = CGFloat(0), bb = CGFloat(0), ba = CGFloat(0)//top and bottom component variables
        topColor.getRed(&tr, green: &tg, blue: &tb, alpha: &ta)
        bottomColor.getRed(&br, green: &bg, blue: &bb, alpha: &ba)
        
        let gradient = CGGradient(colorSpace: CGColorSpaceCreateDeviceRGB(), colorComponents: [tr,tg,tb,ta,br,bg,bb,ba], locations: [0.0, 1.0], count: 2)

        context.drawLinearGradient(gradient!, start: CGPoint(x: 0, y: 0), end: CGPoint(x: rect.size.width, y: rect.size.height), options: CGGradientDrawingOptions.drawsAfterEndLocation)

        context.restoreGState()
    }

}

Here's how it looks:

enter image description here

As you can see, in the bottom square, when height is same as width, it works fine. However, in the top rectangle, when the width is longer than height, it doesn't work correctly as the top right corner doesn't have any red whatsoever and the bottom left corner doesn't have any green whatsoever.

How can I fix this?


Solution

  • In order to get the desired result you need to calculate start and end points that connect a line that is perpendicular to the diagonal line through the rectangle. See the following diagram:

    enter image description here

    The following update to your code calculates the values for dx and dy which can be used to get the start and end points of the gradient.

    class GradientView: UIView {
        override func draw(_ rect: CGRect) {
            let context = UIGraphicsGetCurrentContext()!
    
            let topColor = UIColor.red
            let bottomColor = UIColor.green
    
            let ang = atan(bounds.height / bounds.width)
            let len = cos(ang) * bounds.height
            let dx = sin(ang) * len
            let dy = cos(ang) * len
    
            // Create the gradient using the two colors
            let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: [topColor.cgColor, bottomColor.cgColor] as CFArray, locations: nil)!
    
            // Draw the gradient basing the start and end points off of the
            // center of the rectangle.
            let start = CGPoint(x: bounds.midX - dx, y: bounds.midY - dy)
            let end = CGPoint(x: bounds.midX + dx, y: bounds.midY + dy)
            context.drawLinearGradient(gradient, start: start, end: end, options: [ .drawsBeforeStartLocation, .drawsAfterEndLocation ])
        }
    }
    

    The following sample views (wide, tall, and square) demonstrate the desired output for all three cases:

    let wgv = GradientView(frame: CGRect(x: 0, y: 0, width: 400, height: 100))
    let tgv = GradientView(frame: CGRect(x: 0, y: 0, width: 100, height: 400))
    let sgv = GradientView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
    

    The results are:

    wgv:

    enter image description here

    tgv:

    enter image description here

    and sgv:

    enter image description here