An application cannot offer Light / Dark / System. Native screens follow the system and there is no way to say otherwise, at launch or at runtime.
Asking from HQ.training, where Settings should carry the appearance control almost every iOS app has.
What is there
CraftNativeScreen.swift:5944 decides a screen's scheme:
private var colorScheme: String {
let traits = [view.window?.traitCollection, CraftPresenter.keyWindow()?.traitCollection, traitCollection]
if let style = traits.compactMap({ $0 }).first(where: { $0.userInterfaceStyle != .unspecified })?.userInterfaceStyle {
return style == .dark ? "dark" : "light"
}
return config.appearance == "dark" ? "dark" : "light"
}So there is a config key — craft.config.json carries "appearance": "system" — but it is only reached when every trait collection in that list is .unspecified. On a device one of them always is specified, so the config value is effectively unreachable and the system always wins.
Changes are observed, not controlled: traitCollectionDidChange refreshes theme defaults (:1419) and re-renders (:5399), and sendAppearance publishes an APPEARANCE payload to the script. All of that reports the system's decision.
What is missing
overrideUserInterfaceStyleis never set anywhere in the iOS templates. That is the one UIKit lever that makes an app's own choice stick, and nothing touches it.- No action is registered for it. The dispatcher's closest neighbour is
("StatusBar", "setStyle"), which styles the status bar rather than the interface. - Nothing named
appearanceexists in the published TypeScript surface (craft-native/dist/types.d.ts: 0 matches), so a screen has no way to ask.
An application can therefore style its own views with dark: classes, and those classes resolve against a scheme it cannot influence.
Suggested
Two pieces, and the first is the useful one on its own:
- A runtime setter —
craft.app.setAppearance('system' | 'light' | 'dark'), applyingoverrideUserInterfaceStyleto the key window so every screen, sheet and alert follows.systemmaps to.unspecified, which restores the current behaviour exactly. - Honour
config.appearanceat launch, by applying the same override when it is not"system". Today that key reads as though it works and does nothing, which is worse than not having it.
Persisting the choice belongs to the application — it is a stored preference like any other, and craft.preferences already exists for it. What is missing is only the ability to act on it.
Why it is worth having
Appearance is not decoration for a training application. Phones live in gyms at 6am and beside beds at night, and plenty of people run one app dark and everything else light. It is also among the first settings anyone looks for, so its absence reads as an omission rather than a decision.
Related: this is the same shape as craft-native/craft#387 — the machinery is present and observed, and only the lever to act on it is missing.