What's the difference between UIView and CALayer?

A UIView owns layout, hit-testing, and events; its backing CALayer owns compositing, visual effects, and Core Animation

Answer

The core idea

UIView and CALayer are paired on iOS, but they are not interchangeable. A view is a UIKit object: a rectangular area that participates in the view hierarchy, Auto Layout, and the responder chain. A layer is a Core Animation object: it manages image-based content, visual attributes, and animations. The view is what users tap and constrain; the layer is what Core Animation draws.

1. Every UIView has a backing CALayer

view.layer is never nil. The view creates that layer during initialization, using layerClass to decide the concrete type — CALayer by default. The view assigns itself as the layer's delegate. Apple's warning is specific: do not change view.layer.delegate, and do not make the view the delegate of any other layer.

Most work stays on the view. Reach for the layer when you need rendering or animation that UIKit does not expose as a first-class view API: rounded corners on the backing store, a drop shadow, a border, a custom contents image, or a different layer class.

swift 5.2
import UIKit

final class HeroHeaderView: UIView {
    override class var layerClass: AnyClass {
        CAGradientLayer.self
    }

    private var gradientLayer: CAGradientLayer {
        layer as! CAGradientLayer
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        gradientLayer.colors = [
            UIColor.systemPurple.cgColor,
            UIColor.systemBlue.cgColor,
        ]
    }
}

layerClass is consulted once, early. After the view exists, that backing layer cannot be swapped for a different class. Apple's own example of a non-default class is CATiledLayer for a large scrollable surface.

2. UIView owns hierarchy, layout, and events

A view is the object you add with addSubview(_:), constrain with Auto Layout anchors, and configure for input. Event-related properties such as isUserInteractionEnabled live on the view. Hit-testing walks the view tree with hitTest(_:with:)); touches then travel the responder chain. Gesture recognizers attach to views, not layers.

swift 5.2
import UIKit

final class CardView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .secondarySystemBackground
        isUserInteractionEnabled = true

        let title = UILabel()
        title.translatesAutoresizingMaskIntoConstraints = false
        title.text = "Profile"
        addSubview(title)

        NSLayoutConstraint.activate([
            title.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
            title.centerYAnchor.constraint(equalTo: centerYAnchor),
        ])
    }
}

backgroundColor, alpha, and isHidden are view properties, but they are mirrored onto the backing layer for rendering. Changing them on the view is the UIKit-facing way to change what the layer composites.

3. CALayer owns compositing and visual effects

CALayer manages the pixels and the attributes used to present them: geometry (position, bounds, anchorPoint, transform), contents, opacity, cornerRadius, border, shadow, and masksToBounds. Many of those properties are marked Animatable. Adding an animation with add(_:forKey:) attaches it to the layer's render tree.

A layer can exist without a view. That is useful for decorative drawing — a CAShapeLayer ring, a CAGradientLayer wash — that should not participate in Auto Layout or receive taps.

swift 5.2
import UIKit

func styleCard(_ view: UIView) {
    view.layer.cornerRadius = 12
    view.layer.borderWidth = 1
    view.layer.borderColor = UIColor.separator.cgColor
    view.layer.shadowColor = UIColor.black.cgColor
    view.layer.shadowOpacity = 0.18
    view.layer.shadowOffset = CGSize(width: 0, height: 4)
    view.layer.shadowRadius = 8
}

masksToBounds clips sublayers to the layer's bounds. clipsToBounds is the view-level form of the same clip (subviews). Neither API is about touch handling.

CALayer also has a geometric hitTest(_:). That is not UIKit event delivery. A standalone layer is not a UIResponder, does not sit on the responder chain, and does not receive touchesBegan(_:with:).

Assign cornerRadius or shadowOpacity and the value may ease in even though you never started an animation. Core Animation does that by default for many layer properties. To snap the value immediately, call CATransaction.setDisableActions(true)) before the assignment. Animate view properties such as alpha and frame with UIView.animate(withDuration:animations:). Run a custom layer animation with add(_:forKey:).

4. Extra sublayers skip Auto Layout

Constraints size and position views, not layers. addSublayer(_:) only adds drawing. When the view later grows or shrinks, that extra layer stays where you left it, and a tap on those pixels still hits the view — layers are not in the view hit-test tree.

Keep the extra layer in sync yourself: in layoutSubviews(), set its frame and path from bounds, the same way you would frame a subview by hand.

swift 5.2
import UIKit

final class AvatarView: UIView {
    private let ring = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        ring.fillColor = UIColor.clear.cgColor
        ring.strokeColor = UIColor.systemBlue.cgColor
        ring.lineWidth = 2
        layer.addSublayer(ring)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        // ✅ Path follows Auto Layout size changes
        ring.frame = bounds
        ring.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 2, dy: 2)).cgPath
    }
}

Setting ring.path once in init(frame:) is the usual miss: bounds is often .zero at that moment, and even when it is not, later constraint-driven size changes leave the ring stale.

How to break it: rounding and a shadow on the same layer

This pairing is the interview follow-up, and it is a real UIKit bug, not a trick.

cornerRadius only rounds the layer's background fill. An image or subview still draws as a sharp rectangle until you clip — masksToBounds on the layer, or clipsToBounds on the view.

A drop shadow is painted around the outside of that rectangle. Clipping is a hard crop: it makes the corners follow the radius, and it also crops the shadow away.

The snippet below is intentionally wrong:

swift 5.2
import UIKit

func styleRoundedCard(_ view: UIView) {
    view.layer.cornerRadius = 12
    view.layer.masksToBounds = true // ❌ Clips the shadow
    view.layer.shadowOpacity = 0.18
    view.layer.shadowRadius = 8
    view.layer.shadowOffset = CGSize(width: 0, height: 4)
}

The rule that is violated: one layer cannot both clip to its bounds and show a shadow that lives outside those bounds.

The usual fix is two rectangles — an outer view that draws the shadow and does not clip, and an inner view that clips and rounds:

swift 5.2
import UIKit

func styleRoundedCard(outer: UIView, inner: UIView) {
    outer.backgroundColor = .clear
    outer.layer.shadowOpacity = 0.18
    outer.layer.shadowRadius = 8
    outer.layer.shadowOffset = CGSize(width: 0, height: 4)
    outer.layer.shadowPath = UIBezierPath(
        roundedRect: outer.bounds,
        cornerRadius: 12
    ).cgPath

    inner.clipsToBounds = true
    inner.layer.cornerRadius = 12
}

shadowPath does not solve the clip conflict by itself. It tells Core Animation the shadow's shape so it does not have to derive it from the layer's alpha, which is cheaper to composite. If masksToBounds is still true on that same layer, the path is still clipped. Keep the shadow on the unclipped outer layer, and recompute shadowPath in layoutSubviews() when bounds change.

Why it matters

Interactive UI — buttons, labels, stacks, constraints, taps — is UIView work. Visual polish and extra animation are CALayer work: a gradient, a shadow, a shape overlay, a repeating opacity pulse, or a CABasicAnimation that UIView.animate cannot describe.

Choosing the wrong object shows up immediately: a decoration that ignores Auto Layout, a control that does not receive taps, or an animation written against the API that cannot produce the effect.

Interview angle

Lead with the pairing, then the jobs: every UIView has a backing CALayer (view.layer, never nil); the view is that layer's delegate. The view owns hierarchy, Auto Layout, hit-testing, and events. The layer owns contents, visual effects, and animation. Give one property mapping (clipsToBounds / masksToBounds, alpha / opacity) so it is clear they are two APIs over one render tree. Close with the production split: extra sublayers do not receive touches and are not laid out by constraints; rounded content plus a drop shadow needs two layers (or two views). Apple's pages for UIView.layer and CALayer are the references for the exact contract.