In iOS 18, UIKit automatically tracks when you access a trait, removing the need to manually register for trait change notifications.
Automatic Trait Tracking (iOS 18)
In iOS 18, UIKit supports automatic trait tracking in layout update methods in views and view controllers. See the Apple documentation for the full list of supported methods. It includes UIView
methods like layoutSubviews()
, updatesConstraints()
, and draw(CGRect)
. Supported UIViewController
methods include viewWillLayoutSubviews()
, updateViewConstraints()
, and the updateConfiguration
methods for buttons, collection and table view cells.
Any time UIKit calls one of these methods it notes which traits you access in the method. Then when one of those traits changes it automatically invalidates the view using setNeedsLayout
, setNeedsUpdateConstraints
, setNeedsDisplay
, or setNeedsUpdateConifguration
, as appropriate. This removes the need to manually register for trait changes.
For example, I have a UIView subclass that overrides draw(CGRect)
. My view draws a box, inside the view bounds. When the user chooses one of the accessibility sizes of dynamic type I want to increase the size of my box:
final class SquareView: UIView {
override func draw(_ rect: CGRect) {
var scale = 0.3
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory
{
scale = 0.6
}
let width = bounds.width * scale
let height = bounds.height * scale
let startX = (bounds.width / 2) - (width / 2)
let startY = (bounds.height / 2) - (height / 2)
let path = UIBezierPath()
path.move(to: CGPoint(x: startX, y: startY))
path.addLine(to: CGPoint(x: startX, y: startY + height))
path.addLine(to: CGPoint(x: startX + width, y: startY + height))
path.addLine(to: CGPoint(x: startX + width, y: startY))
path.addLine(to: CGPoint(x: startX, y: startY))
UIColor.blue.setStroke()
UIColor.blue.setFill()
path.stroke()
path.fill()
}
}
The draw(CGRect)
method checks the preferredContentSizeCategory
trait before drawning the box. Before iOS 18, I need to add the view initializers and register for content size category trait changes and then call setNeedsDisplay
to trigger a new call to draw(CGRect)
:
// pre-iOS 18
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
private func setupView() {
registerForTraitChanges([
UITraitPreferredContentSizeCategory.self
],
action: #selector(contentSizeChanged))
}
@objc private func contentSizeChanged() {
setNeedsDisplay()
}
In iOS 18, that’s no longer necessary. UIKit registers that I’m accessing the preferredContentSizeCategory
trait in draw(CGRect)
and automatically calls setNeedsDisplay
anytime it changes.