Search code examples
iosswiftoptimizationdrawinguibezierpath

Drawing a 3D cone shape in Swift using a UIBezierPath() and CAShapeLayer()


Background

I’m attempting to draw a 3D cone shape as shown in the image below.

I have a method that I use to draw a simple triangle and fill it with a solid color in Swift using a UIBezierPath() and CAShapeLayer().


Question

In Swift code, how can I draw a 3D cone shape or fill the triangle shape I’ve drawn with a complex gradient that gives the triangle shape a 3D effect and effectively a cone appearance?


Code

import UIKit
class ViewController: UIViewController {
    @IBOutlet weak var myView: UIView!

    override func viewDidLoad() {
        let path = UIBezierPath()

        path.moveToPoint(CGPoint(x: 0, y: 100))
        path.addLineToPoint(CGPoint(x: 200, y: 100))
        path.addLineToPoint(CGPoint(x: 100, y: 0))
        path.closePath()

        let shape = CAShapeLayer()
        shape.path = path.CGPath
        shape.fillColor = UIColor.grayColor().CGColor

        myView.layer.insertSublayer(shape, atIndex: 0)
    }
}

Image

enter image description here


Solution

  • I was able to achieve your goal making use of CGGradientLayer with a type of conic. The trick is to use the triangle shape as a mask for the gradient.

    Here is an example that can be run in a Swift Playground. Comments in the code.

    import UIKit
    import PlaygroundSupport
    
    class ConeView: UIView {
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            // Just for display
            backgroundColor = .yellow
    
            // Create the triangle to be used as the gradient's mask
            // Base the size of the triangle on the view's frame
            let path = UIBezierPath()
            path.move(to: CGPoint(x: 0, y: frame.height))
            path.addLine(to: CGPoint(x: frame.width, y: frame.height))
            path.addLine(to: CGPoint(x: frame.width / 2, y: 0))
            path.close()
    
            // Create a shape from the path
            let shape = CAShapeLayer()
            shape.path = path.cgPath
    
            // Create a conical gradient and mask it with the triangle shape
            let gradientLayer = CAGradientLayer()
            gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
            gradientLayer.endPoint = CGPoint(x: 0.5, y: -0.5)
            gradientLayer.type = .conic
            // Change the white value of the center color to adjust the center highlight brightness as desired
            gradientLayer.colors = [UIColor.black.cgColor, UIColor(white: 0.9, alpha: 1.0).cgColor, UIColor.black.cgColor]
            // Tweak the 1st and 3rd number as desired.
            // The closer to 0.5 the darker the cone edges will be.
            // The further from 0.5 the lighter the cone edges will be.
            gradientLayer.locations = [ 0.38, 0.5, 0.62 ]
            gradientLayer.frame = bounds
            gradientLayer.mask = shape
            layer.insertSublayer(gradientLayer, at: 0)
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    
    // Set whatever size you want for the view
    PlaygroundPage.current.liveView = ConeView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
    

    Here's the result (the black border is not part of the view, just part of the screen capture):

    enter image description here