uikitcore-graphicscagradientlayer

How would I export figma radial gradient to CAGradientLayer


Figma gradient "description"

css with a confidence inspiring comment

background: radial-gradient(423.17% 112.48% at 32.67% 115.21%, #FFD3D3 7.4%, #E7B9B9 24.14%, #596EBB 58.01%, #382A8B 96.1%) /* warning: gradient uses a rotation that is not supported by CSS and may not behave as expected */; 

relevant part of an svg when the screen is exported:

Which yielded this abomination of a code with startpoint and endpoint I have picked off the wall:

static func shouldMakeGradientLayer(frame: CGRect? = nil) -> CAGradientLayer {
        
        let gradientLayer = CAGradientLayer()

        // Define colors
        gradientLayer.colors = [
            UIColor(hex: 0xFFD3D3).cgColor,
            UIColor(hex: 0xE7B9B9).cgColor,
            UIColor(hex: 0x596EBB).cgColor,
            UIColor(hex: 0x382A8B).cgColor
        ].reversed()

        // Define locations
        //background: radial-gradient(423.17% 112.48% at 32.67% 115.21%, #FFD3D3 7.4%, #E7B9B9 24.14%, #596EBB 58.01%, #382A8B 96.1%) /* warning: gradient uses a rotation that is not supported by CSS and may not behave as expected */;
//      gradientLayer.locations = [0.0739577, 0.241399, 0.580144, 0.961]
//  make a guess :-[    gradientLayer.locations = [0, 0.8, 0.9, 1

        // Apply the gradient transform
        gradientLayer.type = .radial
//      let transform = CATransform3DMakeRotation(-71.2512 * (.pi / 180), 0, 0, 1)
        guard let w = frame?.width, let h = frame?.height else {
            return gradientLayer
        }
//      gradientLayer.transform = CATransform3DConcat(CATransform3DMakeScale(964.534/w, 1675.8/w, 1), transform)
//      gradientLayer.position = CGPoint(x: 122.5, y: 935.5)

        gradientLayer.transform = CATransform3DMakeScale(964.534/w, 1675.8/h, 1)

        let startPoint: CGPoint = CGPoint(x: 1, y: 0)
        let endPoint: CGPoint = CGPoint(x: -2.2, y: 1.4)
        gradientLayer.startPoint = startPoint
        gradientLayer.endPoint = endPoint
        if let frame = frame {
            gradientLayer.frame = frame
        }
        return gradientLayer
    }

Question #1: Is there an easier way to get radial gradient ? or Question #2: Should I have the UI designer isolate the parts of svg that I need as a background and I bite the bullet adding UIImageView as a background in each and every uiviewcontroller that needs it (30+ screens)?

maybe there is some straighforward way to map CSS percentages to locations array?

UPD: Youtube was prompt to suggest to me https://www.youtube.com/watch?v=I4gUvhG7uFU to get me to hate css a little less. But the disclaimer still stands is that I don;t understand css and could use a hand here.

UPD: what needs to be achieved (ignore the icons and the rest of the cruft)

enter image description here


Solution

  • Linear CAGradientLayer ...

    and we get a top-down gradient.

    Radial CAGradientLayer ...

    and we get a Radial gradient starting from the center.

    The general idea -- we start with a radial gradient:

    enter image description here

    Then position it relative to the view frame:

    enter image description here

    and it looks similar to this:

    enter image description here

    For your design, we can set the start point to:

    x: 0.0, y: 1.25
    

    that will put the "center point" of the radial at the left-edge, and 125% of the height (25% past the bottom).

    Then we can set the end point to:

    x: 3.5, y: 0.0
    

    That puts the "outer edge" of the radial at 350% of the width (so, 250% past the right edge), and at the top.

    To make things easier on ourselves, we'll create a UIView subclass, with a few "settable" properties:

    class RadialGradientView: UIView {
    
        public var colors: [UIColor] = [] {
            didSet {
                gradLayer.colors = colors.compactMap( { $0.cgColor } )
            }
        }
        public var locationPcts: [Double] = [] {
            didSet {
                gradLayer.locations = locationPcts.compactMap( { NSNumber(floatLiteral: $0 / 100.0) } )
            }
        }
        public var start: CGPoint = .init(x: 0.5, y: 0.5) {
            didSet {
                gradLayer.startPoint = start
            }
        }
        public var end: CGPoint = .init(x: 1.0, y: 1.0) {
            didSet {
                gradLayer.endPoint = end
            }
        }
    
        override class var layerClass: AnyClass { CAGradientLayer.self }
        private var gradLayer: CAGradientLayer { layer as! CAGradientLayer }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            gradLayer.type = .radial
            
            // defaults, just so we have something to see if colors is not set
            self.start = .init(x: 0.5, y: 0.5)
            self.end = .init(x: 1.0, y: 1.0)
            colors = [.red, .green, .blue, .yellow]
            // leave locations at default
        }
        
    }
    

    If we create a 300x300 instance of that view and add it as a centered subview - with no other settings:

    class SampleViewController: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            
            NSLayoutConstraint.activate([
                v.widthAnchor.constraint(equalToConstant: 300.0),
                v.heightAnchor.constraint(equalTo: v.widthAnchor),
                v.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                v.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])
            
        }
        
    }
    

    it will look like this:

    enter image description here

    If we set some custom colors:

            let v = RadialGradientView()
    
            v.colors = [
                UIColor(hex: 0xFFD3D3),
                UIColor(hex: 0xE7B9B9),
                UIColor(hex: 0x596EBB),
                UIColor(hex: 0x382A8B),
            ]
            
    

    we get this:

    enter image description here

    and custom start/end points:

            let v = RadialGradientView()
    
            v.colors = [
                UIColor(hex: 0xFFD3D3),
                UIColor(hex: 0xE7B9B9),
                UIColor(hex: 0x596EBB),
                UIColor(hex: 0x382A8B),
            ]
            
            v.start = .init(x: 0.0, y: 1.25)
            v.end = .init(x: 3.5, y: 0.0)
    

    We're close to what we want:

    enter image description here

    The next step is to use that as the background of the view controller... of course, probably multiple controllers. And, we don't want to have to add and constrain that as a subview every time.

    So, let's subclass UIViewController:

    class MyCustomBackgroundViewController: UIViewController {
        
        override func loadView() {
            
            let v = RadialGradientView()
            
            v.colors = [
                UIColor(hex: 0xFFD3D3),
                UIColor(hex: 0xE7B9B9),
                UIColor(hex: 0x596EBB),
                UIColor(hex: 0x382A8B),
            ]
            
            // here you can to "tweak" the start, end and locations values
            //  to get your desired appearance
            v.start = .init(x: 0.0, y: 1.25)
            v.end = .init(x: 3.5, y: 0.0)
            //v.locationPcts = [7.4, 24.14, 58.01, 96.1]
            
            self.view = v
        }
        
    }
    

    and, use that class instead of UIViewController for all of our controllers:

    class AnotherViewController: MyCustomBackgroundViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // everything normally as with any UIViewController
            
            let imgView = UIImageView()
            imgView.contentMode = .scaleAspectFit
            imgView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(imgView)
            
            NSLayoutConstraint.activate([
                imgView.widthAnchor.constraint(equalToConstant: 160.0),
                imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
                imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                imgView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])
            
            if let img = UIImage(systemName: "photo") {
                imgView.image = img
                imgView.tintColor = .white
            }
            
        }
        
    }
    

    and (hopefully) we're on our way:

    enter image description here