How do CATransform3D, perspective (m34), zPosition and sublayerTransform work, and what happens to frame?

9.9.2 · L5 · Interview

Predict first

What is the frame of a 100×100 layer rotated 60° about y, with and without perspective?

import UIKit

let L = CALayer(); L.bounds = CGRect(x: 0, y: 0, width: 100, height: 100); L.position = CGPoint(x: 100, y: 100)
L.transform = CATransform3DMakeRotation(.pi / 3, 0, 1, 0)
let f = L.frame
print(String(format: "%.1f %.1f %.1f %.1f", f.minX, f.minY, f.width, f.height), CATransform3DIsAffine(L.transform))
var t = CATransform3DIdentity; t.m34 = -1.0 / 500
L.transform = CATransform3DRotate(t, .pi / 3, 0, 1, 0)
print(L.frame.height > 100, L.frame.width < 100)
let root = CALayer(); var persp = CATransform3DIdentity; persp.m34 = -1.0 / 500
root.sublayerTransform = persp
print(root.sublayerTransform.m34, root.transform.m34, L.isDoubleSided)
  • 75.0 50.0 50.0 100.0 false, then true true, then -0.002 0.0 true
  • 50.0 50.0 100.0 100.0 true, then false false, then 0.0 -0.002 false
  • 75.0 50.0 50.0 100.0 true, then true false, then -0.002 0.0 true

Mental model. A rotated card seen straight on looks narrower; with a vanishing point it also looks like the near side is bigger, like a door swinging toward you.

A layer’s transform is a 4×4 matrix (CATransform3D) applied around its anchor point, so a rotation about the y-axis flips a card over. Without perspective the result is a plain orthographic squash: rotating a 100×100 layer by 60° about y gives a frame 50 wide and 100 tall (cos 60° × 100) and still 100 tall, as the check prints. Perspective is a matrix entry you set yourself: m34 = −1 / d, where d is the distance of the “eye” in points (500 is a common value). With it the near edge grows and the far edge shrinks, and the layer’s frame becomes the bounding box of the projected shape, so it is taller than 100 even though the layer is still 100 points high.

Two placement rules matter. transform on a layer is applied to that layer, but to give children a shared vanishing point set the perspective on the parent’s sublayerTransform (its own transform.m34 stays 0). Depth ordering across siblings uses zPosition, not the order in sublayers; in a tree where parents are flattened, use a CATransformLayer to keep sublayers truly 3D. isDoubleSided (default true) decides whether the back face is drawn. In UIKit, view.transform3D maps to the same matrix. Remember a 3D transform on a view does not change hit testing the way you might expect, and the frame is not a reliable input; reason with bounds, position and the matrix.

Orthographic rotation narrows the frame; perspective makes it taller than the bounds; sublayerTransform carries the perspective for children.

Common traps

  • Putting m34 on each child instead of the parent’s sublayerTransform. Each child then gets its own vanishing point.
  • Setting frame on a layer with a 3D transform. It is derived and unreliable; set bounds and position.

They might follow up with

  • What is m34 = −1/500? A perspective with the eye 500 points away.
  • Why use a CATransformLayer? Its sublayers are not flattened, so 3D structure survives.

Related questions

  • 9.2.2 What are the model, presentation and render layer trees, and how do the layer geometry properties relate?
  • 9.9.1 How does convert(_:to:) work with a changed bounds origin or a transform, and how does a scroll view scroll?
  • 7.2.2 What is the difference between frame and bounds, and what happens to them under a transform?

← How does convert(_:to:) work with a changed bounds origin or a…   How does a layer place its bitmap: contentsGravity, contentsRect… →