Summary
There is no supported way for a Live Activity to display a self-updating countdown at minute granularity in the form transit riders expect — a bare "7 min" that ticks to "6 min" — even though Apple's own apps (Timer in the Dynamic Island, Maps ETA) display exactly this form.
The API that should make this possible — Text(_ input: TimeDataSource<Date>, format:) with a custom DiscreteFormatStyle (added in iOS 18) — compiles and archives, but the rendered activity fails at decode time in the system renderer, so every region of the Live Activity displays grey placeholder boxes.
What I'm building
An NYC subway departures app. The app's design language everywhere (in-app, home screen widgets via baked timeline entries) is "7 min" / "Now". Real-world fidelity is minute-level: riders don't think in mm:ss. The Live Activity is the only surface that cannot match this design, because it can only use self-updating text between content updates, and none of the built-in live formats produce it.
Built-in options and why each falls short
Text(timerInterval:countsDown:) — mm:ss only; no minute-granularity mode (this is FB23091094). Also reserves maximum width (FB23091111).
Text(date, style: .relative) — always appends a second unit ("7 min, 30 sec") and counts up ambiguously (unsigned) after the date passes.
.reference(to:allowedFields:maxFieldCount:) (iOS 18) — closest usable option, but only spelled-out wide units with a mandatory preposition: "in 7 minutes". No abbreviated/narrow width option exists on SystemFormatStyle.DateReference.
.offset(to:) (iOS 18) — measures time since the anchor, so an upcoming departure renders as a negative value ("−7 minutes"); inverted for countdown use, and also wide-units only.
The bug: custom DiscreteFormatStyle fails to decode in the renderer
A custom format style solves this completely — mine rendered exact "7 min"/"Now" strings and, by holding the full departure list, even advanced to the next train the moment one departed, all without waking the app:
@available(iOS 18.0, *)
struct TrainCountdownFormat: DiscreteFormatStyle {
var departures: [Date]
var rank: Int // 0 = closest upcoming train
var fullText: Bool // "7 min" vs "7m"
func format(_ now: Date) -> String {
let upcoming = departures.filter { $0 > now }.sorted()
guard rank < upcoming.count else { return "—" }
let totalSeconds = Int(upcoming[rank].timeIntervalSince(now))
if totalSeconds < 60 { return "Now" }
let minutes = totalSeconds / 60
return fullText ? "\(minutes) min" : "\(minutes)m"
}
func discreteInput(after input: Date) -> Date? {
departures.compactMap { departure -> Date? in
let seconds = departure.timeIntervalSince(input)
guard seconds > 0 else { return nil }
let intoMinute = seconds.truncatingRemainder(dividingBy: 60)
return input.addingTimeInterval(intoMinute > 0 ? intoMinute : 60)
}.min()
}
func discreteInput(before input: Date) -> Date? {
departures.compactMap { departure -> Date? in
let seconds = departure.timeIntervalSince(input)
guard seconds > 0 else {
return departure < input ? departure : departure.addingTimeInterval(-60)
}
let intoMinute = seconds.truncatingRemainder(dividingBy: 60)
return input.addingTimeInterval(intoMinute - 60)
}.max()
}
}
// In the ActivityConfiguration views:
Text(.currentDate, format: TrainCountdownFormat(departures: dates, rank: 0, fullText: true))
This compiles and the activity is created, but every presentation (compact trailing, expanded, lock screen) renders as grey placeholder boxes. The console shows the archive being rejected at decode time in the renderer process:
WidgetRenderer_Activities: (WidgetRenderer) [com.apple.chrono:activityRendererClient-verbose]
Failed to return view entry from archive for view model with tag dynamicIsland-compactTrailing
with error: SwiftUI.AnyCodable<...SafelyCodableRequirement>...Errors.noType(mangledName:
"7SwiftUI18TimeDataFormattingO10ResolvableVy_AA0cD6SourceVAAE11DateStorageOy10Foundation0H0V_G
27NowDepartingWidgetExtension10$1030be9a0yXZ20TrainCountdownFormatV G")
i.e. the archived TimeDataFormatting.Resolvable wrapper references a type defined in the app's widget extension, which the system renderer cannot look up. Nothing in the DiscreteFormatStyle or Text(_:format:) documentation states that only system-defined format styles are renderable in Live Activities, and there is no compile-time or runtime diagnostic surfaced to the developer — the activity just renders blank.
Requests (either would unblock this)
Support custom DiscreteFormatStyle types in Live Activity rendering (e.g. by evaluating the format in the extension's process when pre-rendering discrete frames), or at minimum document the limitation and fail loudly instead of rendering placeholder boxes.
Add minute-granularity and unit-width options to the built-in live formats: a showsSeconds/fields option on Text(timerInterval:) (FB23091094), and/or a units-width option (abbreviated/narrow) plus a "bare duration, no preposition, countdown sign convention" variant on SystemFormatStyle.DateReference / DateOffset.
Steps to reproduce
Create a Live Activity whose views use Text(.currentDate, format:) with any custom DiscreteFormatStyle (sample above).
Start the activity; background the app.
Observe the Dynamic Island and lock screen render grey placeholder boxes for every region, and WidgetRenderer_Activities logs Errors.noType for each view model tag.
Expected: the custom format renders and updates at the boundaries reported by discreteInput(before:/after:), as it does for the system styles.
Actual: decode failure in the renderer; entire activity renders as placeholders.
Environment
Xcode 27.0 (27A266a)
iOS 27.0 simulator, iPhone 18 Pro (also applies to iOS 18+ per API availability)
Widget extension deployment target: iOS 17.6
Related reports: FB23091094, FB23091111, forum thread https://developer.apple.com/forums/thread/834337
0
0
17