How does a layer place its bitmap: contentsGravity, contentsRect, contentsCenter, and how does UIView.contentMode map to them?

9.9.3 · L3 · Interview

Predict first

What gravity does each content mode produce, and what are the layer defaults?

import UIKit

let modes: [UIView.ContentMode] = [.scaleToFill, .scaleAspectFit, .scaleAspectFill, .center, .top, .redraw]
print(modes.map { m -> String in let v = UIView(); v.contentMode = m; return v.layer.contentsGravity.rawValue }.joined(separator: " "))
let l = CALayer()
print(l.contentsRect, l.contentsCenter, l.contentsScale, UIView().contentMode.rawValue)
  • resize resizeAspect resizeAspectFill center bottom resize, then (0.0, 0.0, 1.0, 1.0) (0.0, 0.0, 1.0, 1.0) 1.0 0
  • resize resizeAspect resizeAspectFill center top resize, then (0.0, 0.0, 1.0, 1.0) (0.5, 0.5, 0.0, 0.0) 1.0 0
  • resize resizeAspect resizeAspectFill center bottom redraw, then (0.0, 0.0, 1.0, 1.0) (0.0, 0.0, 1.0, 1.0) 3.0 3

Mental model. Gravity is where you tape the photo on the frame; contentsRect is the window you cut in the mat; contentsCenter is the stretchy middle of a rubber stamp.

A layer’s contents is a bitmap (a CGImage, or what draw(_:) produced). contentsGravity decides how it fits the bounds, and UIView.contentMode is a thin layer over it: .scaleToFill maps to resize, .scaleAspectFit to resizeAspect, .scaleAspectFill to resizeAspectFill, .center to center, and .redraw to resize plus a call to setNeedsDisplay() on every bounds change. One trap shows in the check: .top maps to bottom, because a layer’s own coordinates are bottom-up while UIKit’s are top-down; if you set gravity on a standalone layer on iOS, top and bottom appear reversed from what the names suggest. Defaults: contentsRect is the unit rectangle (use the whole image), contentsCenter is the unit rectangle too (no stretchy middle), and contentsScale is 1 on a bare layer; UIKit sets it to the screen scale for view layers.

The two rectangles are the tools. A contentsRect smaller than the unit rectangle shows only part of the bitmap, which is how sprite sheets and cheap crops work with no new image. A contentsCenter rectangle makes the centre stretch while the edges stay fixed, a nine-slice, the GPU-side twin of UIImage.resizableImage(withCapInsets:) (whose default resizing mode is .tile, not stretch, so pass .stretch for buttons and bubbles). Set contentsScale to the screen scale for any bitmap you supply yourself, or it looks blurry. Because the bitmap is placed by the render server, changing these properties is cheap and animatable.

Content mode .top becomes gravity bottom, .redraw uses resize, and a bare layer has unit rectangles and scale 1.

Common traps

  • Assuming top means up. On a layer it is mirrored from the UIKit meaning; check the mapping.
  • Using resizableImage without a mode. The default tiles the middle instead of stretching it.

They might follow up with

  • Why do sprite sheets use contentsRect? One image, many frames, no new bitmaps.
  • What does .redraw cost? A draw(_:) on every size change.

Related questions

  • 7.2.1 What is the relationship between UIView and CALayer?
  • 9.5.1 How do you draw with Core Graphics and UIBezierPath, and how do scale and caching affect it?
  • 9.11.5 What does an image cost in pixels, what does opaque change in a UIGraphicsImageRenderer, and what do resizing…

← How do CATransform3D, perspective (m34), zPosition and…