Fix: CAGradientLayer Fails to Render in Swift

Fix: CAGradientLayer Fails to Render in Swift

Written by

in

Quick answer: CAGradientLayer fails to render primarily because its colors array requires CGColor instances instead of UIColor, or because its frame was assigned in viewDidLoad using view.frame instead of view.bounds. Map your colors using .map(\..cgColor) and update the layer frame inside viewDidLayoutSubviews(), or subclass UIView and override layerClass to return CAGradientLayer.self.

Why this happens

When a CAGradientLayer fails to appear on screen in a UIKit application, the underlying cause almost always comes down to Core Animation type requirements, layer frame lifecycle issues, or coordinate system mismatches.

The most common error involves assigning UIColor instances directly to the colors property of CAGradientLayer. In Objective-C and Swift, CAGradientLayer.colors is typed as an optional array of arbitrary objects ([Any]?). Because the compiler does not enforce strong typing on the array elements, passing [UIColor.red, UIColor.blue] compiles cleanly. However, Core Animation operates at the Core Graphics level and expects a collection of CGColor references. When the rendering engine encounters raw UIColor instances, it fails to parse the color data and draws an empty, transparent layer without throwing a runtime crash.

The second frequent failure point is incorrect geometry handling. Developers often instantiate and configure a gradient layer inside viewDidLoad(), assigning gradientLayer.frame = view.frame or gradientLayer.frame = view.bounds. At this point in the UIViewController lifecycle, the root view has not completed Auto Layout passes or adapted to screen dimensions. As a result, view.bounds may have a zero size or retain an arbitrary storyboard placeholder size. Furthermore, using view.frame introduces an origin offset based on the superview coordinate space instead of the view’s own local coordinate system.

Finally, CALayers do not automatically resize when their host view bounds change. If you add a sublayer manually and the device rotates or adjusts layout constraints, the gradient remains frozen at its initial size unless explicitly recalculated in layout lifecycle methods.

How to fix it

1. Cast UIColors to CGColor in the Colors Array

Ensure that every color passed into CAGradientLayer.colors is converted to its underlying CGColor representation. Core Animation requires an array of CGColorRef objects wrapped into Swift’s [Any] array.

import UIKit

final class GradientViewController: UIViewController {
    private let gradientLayer = CAGradientLayer()

    override func viewDidLoad() {
        super.viewDidLoad()

        let topColor = UIColor(red: 192.0 / 255.0, green: 38.0 / 255.0, blue: 42.0 / 255.0, alpha: 1.0)
        let bottomColor = UIColor(red: 35.0 / 255.0, green: 2.0 / 255.0, blue: 2.0 / 255.0, alpha: 1.0)

        // Pass CGColor instances, not UIColor instances
        gradientLayer.colors = [topColor.cgColor, bottomColor.cgColor]
        gradientLayer.locations = [0.0, 1.0]
        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)

        view.layer.insertSublayer(gradientLayer, at: 0)
    }
}

If you store colors in an array, use Swift’s map transformation:

let palette: [UIColor] = [.systemBlue, .systemPurple]
gradientLayer.colors = palette.map { $0.cgColor }

2. Update the Layer Frame in viewDidLayoutSubviews

Do not rely on the view dimensions available in viewDidLoad(). Instead, synchronize the gradient layer’s frame with the host view’s bounds inside viewDidLayoutSubviews(). This method executes every time Auto Layout finishes calculating the view geometry, including screen rotations and split-screen size changes.

import UIKit

final class BackgroundGradientViewController: UIViewController {
    private let gradientLayer = CAGradientLayer()

    override func viewDidLoad() {
        super.viewDidLoad()

        gradientLayer.colors = [
            UIColor.systemIndigo.cgColor,
            UIColor.systemTeal.cgColor
        ]
        gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
        gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)

        // Insert at index 0 so it stays behind all UI subviews
        view.layer.insertSublayer(gradientLayer, at: 0)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        // Always assign bounds, never view.frame
        gradientLayer.frame = view.bounds
    }
}

Using view.bounds sets the layer’s origin to (0, 0) within the view’s internal coordinates. Using view.frame introduces the view’s superview origin (such as a navigation bar offset or window position), which pushes the gradient off-center.

3. Subclass UIView and Override layerClass (Best UIKit Practice)

The cleanest, most robust architecture for UIKit is creating a custom UIView subclass whose backing layer is natively a CAGradientLayer. This eliminates manual frame tracking in view controllers entirely, as UIKit automatically manages the backing layer’s frame during layout passes.

import UIKit

public class GradientView: UIView {

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

    public var gradientLayer: CAGradientLayer {
        layer as! CAGradientLayer
    }

    public var colors: [UIColor] = [.systemBlue, .systemIndigo] {
        didSet { updateColors() }
    }

    public var startPoint: CGPoint = CGPoint(x: 0.5, y: 0.0) {
        didSet { gradientLayer.startPoint = startPoint }
    }

    public var endPoint: CGPoint = CGPoint(x: 0.5, y: 1.0) {
        didSet { gradientLayer.endPoint = endPoint }
    }

    public var locations: [NSNumber]? = [0.0, 1.0] {
        didSet { gradientLayer.locations = locations }
    }

    public override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }

    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        configure()
    }

    private func configure() {
        updateColors()
        gradientLayer.startPoint = startPoint
        gradientLayer.endPoint = endPoint
        gradientLayer.locations = locations
    }

    private func updateColors() {
        gradientLayer.colors = colors.map { $0.cgColor }
    }

    public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
            updateColors()
        }
    }
}

Using this custom view in your view controller or Storyboard requires no layout maintenance:

final class ProfileViewController: UIViewController {
    private let backgroundView = GradientView()

    override func viewDidLoad() {
        super.viewDidLoad()

        backgroundView.colors = [.systemPurple, .systemOrange]
        backgroundView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(backgroundView)
        view.sendSubviewToBack(backgroundView)

        NSLayoutConstraint.activate([
            backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
            backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

4. Use Native LinearGradient in SwiftUI

If you are writing new code in SwiftUI or bridging a UIKit view to modern declarations, avoid CAGradientLayer altogether and use SwiftUI’s native gradient styling.

import SwiftUI

struct GradientBackgroundView: View {
    var body: some View {
        ZStack {
            LinearGradient(
                colors: [Color.red, Color.black],
                startPoint: .top,
                endPoint: .bottom
            )
            .ignoresSafeArea()

            Text("Content goes here")
                .foregroundColor(.white)
        }
    }
}

For simple background modifiers on views, you can apply .background directly:

Text("Hello, World!")
    .padding()
    .background(
        .linearGradient(
            colors: [.indigo, .cyan],
            startPoint: .topLeading,
            endPoint: .bottomTrailing
        )
    )

How to prevent it

  • Always map UIColor collections to CGColor before assigning to CAGradientLayer.colors (colors.map(\..cgColor)).
  • Never assign a sublayer’s frame in viewDidLoad(); defer manual layer sizing to viewDidLayoutSubviews().
  • Always set sublayer.frame = hostView.bounds, never sublayer.frame = hostView.frame.
  • Prefer subclassing UIView with override class var layerClass: AnyClass { CAGradientLayer.self } over manual sublayer management to ensure Auto Layout responsiveness.
  • Re-apply CGColor assignments inside traitCollectionDidChange or register for dynamic trait changes so dynamic system colors update correctly when switching between Light Mode and Dark Mode.
  • Ensure insertSublayer(_:at:) is used with index 0 if you want the gradient positioned underneath other controls.

FAQ

Why does my gradient turn solid or disappear in Dark Mode?

CGColor references do not automatically resolve dynamic system colors when the interface style changes. To support Dark Mode, listen for appearance changes in traitCollectionDidChange or your view controller’s trait update methods, resolve the dynamic UIColor against the active trait collection, and reassign the cgColor values to the layer.

What is the difference between view.frame and view.bounds when setting layer geometry?

view.frame describes a view’s location and size relative to its parent superview, whereas view.bounds describes the view’s size with its own origin set to (0, 0). Assigning gradientLayer.frame = view.frame shifts the gradient layer within its own coordinate space by whatever X and Y offsets the parent applied, pushing the gradient out of view.

How do CAGradientLayer startPoint and endPoint coordinates work?

Both startPoint and endPoint use unit coordinate space ranging from 0.0 to 1.0. The point (0, 0) is the top-left corner, and (1, 1) is the bottom-right corner. A vertical gradient uses startPoint = (0.5, 0.0) and endPoint = (0.5, 1.0), while a diagonal gradient uses (0.0, 0.0) to (1.0, 1.0).

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.