3

I'm trying to create a radial CAGradientLayer in swift. Normal CAGradientLayers (with default type axial) are no problem. They are rendering fine on the snapshot and on runtime in the simulator.

When I take a snapshot of the UIView that I created (solely for the CAGradientLayer), it shows a beautiful radial CAGradientLayer. However, when I use this view and look at it at runtime in the simulator, it looks awful. I don't know what I'm doing wrong.

This is the code for the CAGradientLayer:

import Foundation

class ChannelGradientView: UIView {

  // MARK: - Init

  override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
  }

  // MARK: - Setup

  private func setup() {
    backgroundColor = .clear
    setupGradient()
  }

  private func setupGradient() {
    let gradient = layer as? CAGradientLayer
    gradient?.type = .radial

    gradient?.colors = [
      UIColor.black.withAlphaComponent(0.8),
      UIColor.black.withAlphaComponent(0)
    ].map { $0.cgColor }

    let blackPoint = CGPoint(x: 1, y: 0)
    let clearPoint = CGPoint(x: 1, y: 1)

    // startpoint is center (for example black here)
    gradient?.startPoint = blackPoint
    gradient?.endPoint = clearPoint
  }

  // MARK: - Layer

  override public class var layerClass: Swift.AnyClass {
    return CAGradientLayer.self
  }
}

This is what it looks like when I take a snapshot unit test (with Nimble Snapshot library):

enter image description here

And this is what it looks like at runtime on a simulator:

enter image description here enter image description here

Anyone that has an idea of what I'm doing wrong?

EDIT: Running at an actual device, it doesn't even show any gradient layer, not even the crappy one. It's a clear box.

Charlotte1993
  • 589
  • 4
  • 27
  • Not quite clear what you're showing... Do you have the gradient view overlaid on top of an image view? So you want the bottom-left-corner of the image to be "unchanged"? – DonMag Sep 11 '19 at 13:20
  • No the image view is on top of the gradient view. You can think the image view away in fact. It has no connection whatsoever with the gradient view. – Charlotte1993 Sep 11 '19 at 13:23

2 Answers2

6

Not sure what else might be going on between what you see on Simulator vs Device, but...

If I use your code as-is, I get this (red border to show the frame):

enter image description here

If I change your clearPoint:

//let clearPoint = CGPoint(x: 1, y: 1)
let clearPoint = CGPoint(x: 0, y: 1)    // bottom-left-corner

I get this:

enter image description here

Appearance is the same on Simulator and Device


EDIT

Some additional clarification...

The Radial Gradient doesn't use .startPoint and .endPoint in the same way that an .axial (linear) gradient does.

With .radial, a gradient *ellipse is drawn, using .startPoint as its center, and the difference between startPoint.x and endPoint.x times 2 as its width and the difference between startPoint.y and endPoint.y times 2 as its height.

So, to get the top-right to bottom-left radial gradient you want, you need to set .startPoint to 1,0 and the .endPoint to values which result in a gradient oval of size 2 x 2:

startPoint = 1,0

endPoint = 0,1

    width:  abs(1 - 0) * 2 = 2
    height: abs(0 - 1) * 2 = 2

note that you can achieve the same result with:

startPoint = 1,0

endPoint = 2,-1

    width:  abs(1 - 2) * 2 = 2
    height: abs(0 - (-1)) * 2 = 2

What the .radial gradient doesn't like is to have its width or height set to Zero, which is what we got with:

startPoint = 1,0

endPoint = 1,1

    width:  abs(1 - 1) * 2 = 0   // problem!!!!
    height: abs(0 - 1) * 2 = 2

The result, as we've seen, is the weird line pattern.

Here are some practical examples to demonstrate.

Axial / Linear top-right to bottom-left:

enter image description here

Radial centered and width = view width / height = view height:

enter image description here

Radial centered and width = one-half view width / height = view height:

enter image description here

Radial centered and width = one-half view width / height = view height... Exact same result, but note that the .endPoint.x value is 0.75 instead of 0.25:

enter image description here

Now we change .startPoint.x = 0.75, but we leave .endPoint.x = 0.25, so the center of the ellipse moves to 3/4ths of the width of the view, but the width of the ellipse becomes equal to the width of the view... abs(0.75 - 0.25) * 2 == 1.0:

enter image description here

Change .endPoint.x = 0.5 and the width returns to 1/2 the width of the view... abs(0.75 - 0.5) * 2 = 0.5:

enter image description here

And finally radial gradient from top-right to bottom-left:

enter image description here

Here is the code I used to generate these images. It has a data-block of "Gradient Definitions"... You can add / change those definitions to experiment with the differences.

//
//  GradTestViewController.swift
//
//  Created by Don Mag on 9/12/19.
//

import UIKit

class TestGradientView: UIView {

    // MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    // MARK: - Setup
    private func setup() {

        // just set the background to clear and the
        //  gradient colors to blue -> green
        backgroundColor = .clear

        if let gradient = layer as? CAGradientLayer {
            gradient.colors = [
                UIColor.blue,
                UIColor.green,
                ].map { $0.cgColor }
        }

    }

    // MARK: - Layer
    override public class var layerClass: Swift.AnyClass {
        return CAGradientLayer.self
    }

}

struct GradDef {
    var gradType: CAGradientLayerType = .axial
    var startPoint: CGPoint = CGPoint(x: 1.0, y: 0.0)
    var endPoint: CGPoint = CGPoint(x: 0.0, y: 1.0)
}

class GradTestViewController: UIViewController {

    var theButton: UIButton = {
        let v = UIButton()
        v.setTitle("Tap", for: .normal)
        v.setTitleColor(.white, for: .normal)
        v.setTitleColor(.lightGray, for: .highlighted)
        v.backgroundColor = .red
        return v
    }()

    var counterLabel: UILabel = {
        let v = UILabel()
        return v
    }()

    var descLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        return v
    }()

    var gradContainerView: UIView = {
        let v = UIView()
        return v
    }()

    var gradView: TestGradientView = {
        let v = TestGradientView()
        return v
    }()

    var tlLabel: UILabel = {
        let v = UILabel()
        v.text = "0,0"
        return v
    }()

    var trLabel: UILabel = {
        let v = UILabel()
        v.text = "1,0"
        return v
    }()

    var blLabel: UILabel = {
        let v = UILabel()
        v.text = "0,1"
        return v
    }()

    var brLabel: UILabel = {
        let v = UILabel()
        v.text = "1,1"
        return v
    }()

    var theStackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.alignment = .center
        v.distribution = .fill
        v.spacing = 20.0
        return v
    }()

    var gradDefs: [GradDef] = [
        GradDef(gradType: .axial,  startPoint: CGPoint(x: 1.0,  y: 0.0), endPoint: CGPoint(x: 0.0,  y: 1.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.5,  y: 0.5), endPoint: CGPoint(x: 0.0,  y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.5,  y: 0.5), endPoint: CGPoint(x: 0.25, y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.5,  y: 0.5), endPoint: CGPoint(x: 0.75, y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.75, y: 0.5), endPoint: CGPoint(x: 0.25, y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.75, y: 0.5), endPoint: CGPoint(x: 1.0,  y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 1.0,  y: 0.0), endPoint: CGPoint(x: 0.0,  y: 1.0)),
    ]

    var idx: Int = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        [theButton, counterLabel, gradContainerView, gradView, tlLabel, trLabel, blLabel, brLabel, descLabel, theStackView].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        [theButton, counterLabel, gradContainerView, descLabel].forEach {
            theStackView.addArrangedSubview($0)
        }

        [gradView, tlLabel, trLabel, blLabel, brLabel].forEach {
            gradContainerView.addSubview($0)
        }

        [counterLabel, tlLabel, trLabel, blLabel, brLabel, descLabel].forEach {
            $0.font = UIFont.monospacedDigitSystemFont(ofSize: 14.0, weight: .regular)
        }

        NSLayoutConstraint.activate([

            gradView.widthAnchor.constraint(equalToConstant: 120),
            gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor),
            gradView.centerXAnchor.constraint(equalTo: gradContainerView.centerXAnchor),
            gradView.centerYAnchor.constraint(equalTo: gradContainerView.centerYAnchor),

            tlLabel.centerXAnchor.constraint(equalTo: gradView.leadingAnchor, constant: 0.0),
            blLabel.centerXAnchor.constraint(equalTo: gradView.leadingAnchor, constant: 0.0),
            trLabel.centerXAnchor.constraint(equalTo: gradView.trailingAnchor, constant: 0.0),
            brLabel.centerXAnchor.constraint(equalTo: gradView.trailingAnchor, constant: 0.0),

            tlLabel.bottomAnchor.constraint(equalTo: gradView.topAnchor, constant: -2.0),
            trLabel.bottomAnchor.constraint(equalTo: gradView.topAnchor, constant: -2.0),

            blLabel.topAnchor.constraint(equalTo: gradView.bottomAnchor, constant: 2.0),
            brLabel.topAnchor.constraint(equalTo: gradView.bottomAnchor, constant: 2.0),

            tlLabel.topAnchor.constraint(equalTo: gradContainerView.topAnchor, constant: 4.0),
            tlLabel.leadingAnchor.constraint(equalTo: gradContainerView.leadingAnchor, constant: 4.0),

            brLabel.trailingAnchor.constraint(equalTo: gradContainerView.trailingAnchor, constant: -4.0),
            brLabel.bottomAnchor.constraint(equalTo: gradContainerView.bottomAnchor, constant: -4.0),

            ])

        view.addSubview(theStackView)

        NSLayoutConstraint.activate([

            theStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
            theStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),

            theButton.widthAnchor.constraint(equalToConstant: 160.0),
            descLabel.widthAnchor.constraint(equalToConstant: 240.0),

            ])

        theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)

        idx = -1
        didTap(nil)
    }

    @IBAction func didTap(_ sender: Any?) {

        guard let gLayer = gradView.layer as? CAGradientLayer else {
            fatalError("Could not get the gradient layer!")
        }

        idx += 1

        if idx >= gradDefs.count {
            idx = 0
        }

        let gDef = gradDefs[idx]

        gLayer.type = gDef.gradType
        gLayer.startPoint = gDef.startPoint
        gLayer.endPoint = gDef.endPoint

        var s = ""
        s += "Gradient Type: " + (gDef.gradType == CAGradientLayerType.axial ? "Axial" : "Radial")
        s += "\n\n"
        s += "Start Point: \(gDef.startPoint)"
        s += "\n"
        s += "End Point:   \(gDef.endPoint)"

        if gDef.gradType == CAGradientLayerType.radial {
            let w = abs(gDef.startPoint.x - gDef.endPoint.x) * 2
            let h = abs(gDef.startPoint.y - gDef.endPoint.y) * 2
            s += "\n\n"
            s += "\t" + "Radial Width:"
            s += "\n"
            s += "\t\t" + "abs(\(gDef.startPoint.x) - \(gDef.endPoint.x)) * 2 == \(w)"
            s += "\n\n"
            s += "\t" + "Radial Height:"
            s += "\n"
            s += "\t\t" + "abs(\(gDef.startPoint.y) - \(gDef.endPoint.y)) * 2 == \(h)"
        }

        s += "\n"

        descLabel.text = s

        counterLabel.text = "Variation \(idx + 1) of \(gradDefs.count)"

    }

}

Everything's done in code - no @IBOutlets or @IBActions, so just create a new view controller and assign its Custom Class to GradTestViewController.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Wow, okay, that might be the solution :D Thanks :D Will try it out right away. But it's really weird that the snapshot was showing the image below and not the striped image though... – Charlotte1993 Sep 11 '19 at 13:42
  • @Charlotte1993 - I updated my answer with some additional clarification on using Radial Gradients. – DonMag Sep 12 '19 at 14:49
0

here is the fix, set your point as follows:

    let blackPoint = CGPoint(x: 1.0, y: 0.0)
    let clearPoint = CGPoint(x: 0.0, y: 1.0)

Tell me if its ok in your environment.

--

I put your whole code in a playground as follows, where I made some adjustments (I stuck the gradient in the right top corner):

//: A UIKit based Playground for presenting user interface

import UIKit
import PlaygroundSupport



class ChannelGradientView: UIView {

  // MARK: - Init

  override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
  }

  // MARK: - Setup

  private func setup() {
    backgroundColor = .clear
    setupGradient()
  }

  private func setupGradient() {
    guard let gradient = layer as? CAGradientLayer else { return }
    //let gradient = CAGradientLayer()
    gradient.type = .radial
    gradient.frame = frame
    gradient.colors = [
        UIColor.black.cgColor,
        UIColor.clear.cgColor
    ]

    let blackPoint = CGPoint(x: 1.0, y: 0.0)
    let clearPoint = CGPoint(x: 0.0, y: 1.0)

    // startpoint is center (for example black here)
    gradient.startPoint = blackPoint
    gradient.endPoint = clearPoint
  }

  // MARK: - Layer

  override public class var layerClass: Swift.AnyClass {
    return CAGradientLayer.self
  }
}


class MyViewController : UIViewController {
    override func loadView() {
        let view = UIView()
        self.view = view

        view.backgroundColor = .white

        let frame = CGRect(x: 150, y: 200, width: 200, height: 200)

        let iv = UIImageView()
        iv.frame = frame
        iv.alpha = 0.8
        iv.contentMode = .scaleAspectFill
        if let url = URL(string: "https://images-na.ssl-images-amazon.com/images/I/61VpNkHPRoL._SX679_.jpg"),
            let img = try? Data(contentsOf: url) {
            iv.image = UIImage(data: img)
        }
        view.addSubview(iv)
        iv.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            iv.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        iv.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        iv.topAnchor.constraint(equalTo: view.topAnchor),
        iv.bottomAnchor.constraint(equalTo: view.bottomAnchor),

        ])


        let gd = ChannelGradientView()
        view.addSubview(gd)
        gd.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
        gd.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        gd.topAnchor.constraint(equalTo: view.topAnchor),
        gd.widthAnchor.constraint(equalTo: view.widthAnchor),
        gd.heightAnchor.constraint(equalTo: gd.widthAnchor),

        ])
    }
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

And it does display properly:

enter image description here

Stéphane de Luca
  • 12,745
  • 9
  • 57
  • 95