Sitemap

Why Your UIKit Border Colors Break in Dark Mode (And How to Fix It)

2 min readJun 14, 2026

--

The hidden gotcha with CGColor that every iOS developer should know

Press enter or click to view image in full size

The Bug

I had a custom segmented control that looked perfect in light mode.

White container, purple border on the selected tab, everything matching our Figma design.

Then QA tested dark mode.

The borders vanished.

// This looks correct, but it's broken
backgroundContainer.layer.borderColor = UIColor.style.l1Divider.cgColor
selectedIndicator.layer.borderColor = UIColor.style.b1BrandColor.cgColor

The Problem: CGColor Doesn’t Auto-Update

Here’s what most iOS developers don’t realize:

UIColor is dynamic. CGColor is not.

When you write:

view.layer.borderColor = UIColor.systemGray.cgColor

The .cgColor is evaluated once, at that exact moment, for the current trait collection.

When the system switches to dark mode:

  • UIColor.systemGray would return a different color
  • But your layer.borderColor is already set and won't update

This affects:

  • layer.borderColor
  • layer.shadowColor
  • layer.backgroundColor (when set via CALayer, not UIView)
  • Any other CGColor property on CALayer

The Fix: traitCollectionDidChange

Override traitCollectionDidChange to manually update your CGColor values:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)

// Only update if the color appearance actually changed
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
updateColors()
}
}

private func updateColors() {
// Re-evaluate UIColor and get new cgColor for current appearance
backgroundContainer.layer.borderColor = UIColor.style.l1Divider.cgColor
selectedIndicator.layer.borderColor = UIColor.style.b1BrandColor.cgColor
}

Why hasDifferentColorAppearance?

traitCollectionDidChange is called for any trait change—size classes, layout direction, display scale, etc.

We only care about color changes.

hasDifferentColorAppearance(comparedTo:) specifically checks if:

  • userInterfaceStyle changed (light/dark)
  • accessibilityContrast changed (normal/high)
  • userInterfaceLevel changed (base/elevated)

Complete Example

final class CustomSegmentedControl: UIView {

private let backgroundContainer = UIView()
private let selectedIndicator = UIView()
private var buttons: [UIButton] = []
private var selectedIndex: Int = 0

// ... init and setup code ...

private func setupView() {
// Initial color setup
backgroundContainer.layer.borderWidth = 1
backgroundContainer.layer.borderColor = UIColor.style.l1Divider.cgColor

selectedIndicator.layer.borderWidth = 1
selectedIndicator.layer.borderColor = UIColor.style.b1BrandColor.cgColor

// ... rest of setup ...
}

// MARK: - Dark Mode Support

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)

if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
updateColors()
}
}

private func updateColors() {
// Update layer border colors
backgroundContainer.layer.borderColor = UIColor.style.l1Divider.cgColor
selectedIndicator.layer.borderColor = UIColor.style.b1BrandColor.cgColor

// Update button text colors for consistency
for (index, button) in buttons.enumerated() {
if index == selectedIndex {
button.setTitleColor(.style.b1BrandColor, for: .normal)
} else {
button.setTitleColor(.style.t3Body, for: .normal)
}
}
}
}

This bug cost me 2 hours of debugging. Hopefully this article saves you that time.

Thank you for reading! If you enjoyed it, please consider clapping and following for more updates! 👏

--

--