Dynamic UIColor from asset catalog doesn't update CALayer.borderColor when switching between light and dark mode — modern workaround?

1 day ago 2
ARTICLE AD BOX

I'm implementing dark mode in a UIKit app and ran into the well-known issue that CALayer.borderColor (and shadowColor, CAShapeLayer.strokeColor, etc.) doesn't update automatically when the user toggles between light and dark mode.

How my colors are defined

All my colors live in an .xcassets Color Set with both an "Any Appearance" and a "Dark Appearance" variant defined directly in the asset catalog, e.g.:

Colors.xcassets/
└── BorderColor.colorset/
├── Any Appearance → #D1D5DB
└── Dark Appearance → #3F3F46

I expose them through a generated namespace, so call sites look like:
let borderColor: UIColor = AppColors.borderColor // resolves dynamically

UIColor.backgroundColor, UILabel.textColor, UIImageView.tintColor all update perfectly when the system trait collection changes — that part works as expected.

The problem

The moment I assign one of these dynamic colors to a CALayer property, it's converted to a static CGColor:

final class RoundedBorderView: UIView { override init(frame: CGRect) { super.init(frame: frame) layer.borderWidth = 1 layer.borderColor = AppColors.borderColor.cgColor // ← frozen here layer.cornerRadius = 12 } required init?(coder: NSCoder) { fatalError() } }

When the user switches appearance, the border keeps its original (light-mode) color forever, because CALayer isn't a UITraitEnvironment and a CGColor carries no appearance information.

What I've tried

The standard fix is to re-apply cgColor on trait changes, e.g. on iOS 17+:

registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: RoundedBorderView, _) in view.layer.borderColor = AppColors.borderColor.cgColor }

…or override traitCollectionDidChange(_:) on older OS versions.

This works, but in a non-trivial app I have dozens of views, cells, and custom controls that draw borders, shadows, or shape strokes. Sprinkling registerForTraitChanges (or a traitCollectionDidChange override) into every single one of them feels like a lot of boilerplate just to compensate for the fact that a layer property can't observe its own environment.

The actual question

Is there a more modern API (or any sanctioned pattern from Apple) that lets me bind a dynamic UIColor from an asset catalog directly to CALayer.borderColor (or shadowColor, CAShapeLayer.strokeColor, gradient colors, etc.) so it re-resolves automatically on trait changes — without me wiring up a trait observer in every view that happens to have a
border?

I'm looking for either:

An Apple-blessed API I've missed (something analogous to UIView.tintColor propagation, but for layer colors), or

Confirmation that no such thing exists and the centralized-extension/subclass pattern really is the state of the art in 2026.

Any insight from people who've shipped large UIKit apps with dark mode would be much appreciated.


Environment: Xcode 26.3, minimum deployment target iOS 16, app primarily uses UIKit (no SwiftUI).

Read Entire Article