Skip to content

fix(theme): honour the AccentColor asset app-wide - #113

Merged
Adron merged 1 commit into
mainfrom
fix/accent-color-honoured
Sep 17, 2026
Merged

Adron merged 1 commit into
mainfrom
fix/accent-color-honoured

Conversation

@Adron

@Adron Adron commented Sep 17, 2026

Copy link
Copy Markdown
Member

Summary

Closes #102. The AccentColor asset was configured correctly all along — it was being destroyed at
runtime by the app's own launch code. Fixing that makes the global accent work, which is strictly
better than explicit tinting: the accent is the process tint, so it reaches sheets, alerts and
UIKit-hosted chrome that a .tint modifier cannot. The two hierarchy-wide .tint workarounds are
removed, and the convention is documented in CLAUDE.md.

Root cause

Not the colorset, not the build setting, not Info.plist. All three were verified correct against
the built product: NSAccentColorName = AccentColor is present in InterlinedList.app/Info.plist,
and assetutil --info Assets.car shows the AccentColor colour with both the light and dark
appearance variants compiled in. UIColor(named: "AccentColor") resolves to brand green at runtime.

The cause is configureNavigationBarAppearance() being called from InterlinedListApp.init().
Isolated statement by statement against a live simulator, the trigger is exactly this pair:

let back = UIBarButtonItemAppearance()
back.normal.titleTextAttributes = [.foregroundColor: UIColor.white]

Materialising a bar-button state appearance that early resolves UIKit's default bar-button styling
before UIApplication has adopted the accent asset. UIKit caches the system-blue fallback as the
process tint for the rest of the launch, so UIColor.tintColor — and therefore SwiftUI's
Color.accentColor — stays blue forever. Constructing UIBarButtonItemAppearance() without
touching .normal, writing UINavigationBar.appearance().tintColor, assigning a
UINavigationBarAppearance to the proxy, and simply reading UINavigationBar.appearance() are all
harmless on their own; only reaching into the state appearance from init() breaks it.

Differential check: a throwaway single-view app with a red AccentColor, built with the same Xcode
and run on the same simulator, honours its accent — and still honours it after being given
InterlinedList's own Info.plist verbatim with GENERATE_INFOPLIST_FILE = NO. So this was never a
project-configuration or iOS-version problem.

What's included

  • configureNavigationBarAppearance() moves from App.init() to
    AppDelegate.application(_:didFinishLaunchingWithOptions:). The teal masthead, white title and
    white back-button styling are unchanged; the accent now survives.
  • RootView's .tint(ILColor.link) (from feat(theme): apply the saved theme before the account loads #101) and the deep-link sheet's .tint(ILColor.link) are
    removed. Controls now render ILColor.primary — the brand green the asset has always held, and
    the colour the app already uses for actions in 30+ places.
  • InterlinedListTests/ServiceTests/AppAccentColorTests.swift (5 tests): the Info.plist declares the
    asset, the asset matches ILColor.green / ILColor.greenDark in each appearance, and the process
    tint equals the accent rather than system blue. The last two fail on the pre-fix code — verified by
    putting the old init() back — so they are a real guard, not a tautology.
  • CLAUDE.md gains the convention: brand tint comes from the asset, a new sheet needs no .tint,
    and UIAppearance must not be configured from App.init().

Bonus fix: the deep-link sheet's explicit tint was also a contrast bug. ILColor.link is #184860,
the same colour as the masthead, so the sheet's "Cancel" was dark teal text on a dark teal bar —
invisible. Screenshots of both states are in the testing notes below.

Sheets audited

64 .sheet presentations across 22 files; no .fullScreenCover anywhere. Exactly one carried a
tint (the deep-link sheet in InterlinedListApp) — every other one relied on RootView's .tint
reaching it, which it did for sheets presented from inside RootView's subtree and did not for the
one presented above it. All of them now inherit the accent with no per-sheet modifier:

File Sheets Presents
InterlinedListApp.swift 1 deep-link router (reset password, profile, message, document, list, public list/doc, shared list/doc) — was the only tinted one
Views/ListsView.swift 12 create list, rename ×2, schema editor ×2, create-from, compose-from-row, sharing, add connection, add item, edit item, contributors
Views/DocumentsView.swift 10 create doc ×2, template picker ×2, AI-powered doc, create folder ×2, edit doc, create-from, sharing
Views/FeedView.swift 8 create-from, thread, profile, scheduled, repost, edit, report, compose
Views/MessageDetailView.swift 6 reply, repost, thread, profile, report, create-from
Views/MainTabView.swift 4 messages inbox, notifications, edit profile, settings
Views/UserProfileView.swift 3 report, share sheet, DM thread
Views/OrganizationsView.swift 3 create org, edit org, add member
Views/EditProfileView.swift 2 avatar URL entry, change email
Views/LoginView.swift 2 register, forgot password
Views/MessageThreadView.swift 2 reply compose, report
Views/AISeriesGeneratorSection.swift 1 AI series sheet
Views/AIWritingAssistantButton.swift 1 AI suggestion sheet
Views/DocumentCollaboratorsView.swift 1 add collaborator
Views/FindPeopleView.swift 1 profile
Views/FollowListView.swift 1 profile
Views/MessagesInboxView.swift 1 DM recipient picker
Views/NotificationsView.swift 1 notification detail
Views/RegisterView.swift 1 terms (Safari)
Views/ScheduledMessagesView.swift 1 edit scheduled message
Views/SettingsView.swift 1 Safari link
Views/WatchersListView.swift 1 add watcher

The remaining .tint(...) call sites in Views/ are deliberately local and were left alone:
swipe-action background colours (ListsView, DocumentsView, AppDevicesView,
MessagesInboxView) and a ProgressView on a dark image overlay (ComposeImageStrip).

Testing

  • Full suite on iPhone 16 Pro (iOS 18.0), serialized, E2E skipped: 1325 tests, 0 failures
    (1320 baseline + 5 new).
  • Build clean, no new warnings.
  • Visual, simulator, both appearances via xcrun simctl ui <UDID> appearance light|dark:
    • Login screen — brand green #2FA877 in light, #3FBF8C in dark; teal masthead and white title
      unchanged. Sampled the rendered pixels: system blue #007AFF before, brand green after.
    • Deep-link sheet (reset password, reached by seeding router.pendingDeepLink, reverted before
      commit) — "Cancel" renders brand green in both appearances with no tint of its own. On main
      the same button is invisible: ILColor.link #184860 on the #184860 masthead.
  • Regression-guard check: restoring the old App.init() makes
    test_processTint_isTheAccentAsset_notSystemBlue_* fail; the fix makes them pass.

Not verified

  • Authenticated surfaces. No account was available, so everything behind login — feed, lists,
    documents, settings, and the 60-odd sheets those screens present — was not seen rendered. The
    mechanism is process-wide rather than per-view (UIColor.tintColor is now the accent, asserted by
    the new tests), so they should all follow, but that is inference, not a screenshot.
  • Physical device and iPad; only the iPhone 16 Pro simulator on iOS 18.0 was used.
  • The simulator gates simctl openurl on a custom scheme behind a SpringBoard confirmation that
    cannot be tapped headlessly, so the deep-link sheet was reached by temporarily seeding the router
    instead of through a real deep link. The temporary seed is not in this branch.

🤖 Generated with Claude Code

Configuring UINavigationBar's appearance from `App.init()` builds a
UIBarButtonItemAppearance before UIApplication has adopted the AccentColor
asset. That resolves UIKit's default bar-button styling early and caches
system blue as the process tint for the rest of the launch, so the asset was
ignored and every control without an explicit tint rendered iOS blue.

Moving the configuration to the app delegate's launch callback keeps the teal
masthead and lets the accent apply, which reaches surfaces a `.tint` cannot:
sheets, alerts and UIKit-hosted chrome. The hierarchy-wide `.tint` workarounds
on RootView and the deep-link sheet are removed — the latter also rendered its
Cancel button in the masthead colour against the masthead, i.e. invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bss5MgZa7Jvj2m9zdaUd1
@Adron
Adron merged commit 7519e51 into main Sep 17, 2026
1 check passed
@Adron
Adron deleted the fix/accent-color-honoured branch September 17, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AccentColor is configured correctly but not honoured — system controls fall back to blue

1 participant