ISO8601DateFormatter storage vs NSDateFormatter local display for a cross-timezone event app
0 reputation · 11 Dec 2020, 20:36 UTC
0 reputation · 11 Dec 2020, 20:36 UTC
I am designing date handling for an iOS app (Xcode 15+, Swift, targeting iOS 15 and later) that stores event times on a server and displays them to users who travel across regional time zones. The hard constraint is that stored values must round-trip deterministically, while the UI must always reflect the user's current device time zone.
One option is to persist everything with ISO8601DateFormatter (or Codable's ISO8601 strategy) in UTC and convert to local only at display time with NSDateFormatter configured with locale and timeZone. The other is to store the user's local wall-clock time plus a time-zone identifier, preserving the original intent (e.g., "9 AM Tokyo") even if the user later moves.
My uncertainty: Calendar.current.component(_:from:) silently follows the device's default time zone, so any grouping logic (e.g., "events today") can shift when the user changes regions, and DST transitions risk off-by-one-day errors in either model.
Calendar.current?NSDateFormatter.defaultTimeZone with fixed ISO8601 strings on older iOS versions?35275 reputation · 12 Dec 2020, 07:38 UTC
For location-anchored events, neither pure UTC alone nor pure local time alone is the right model. The well-established pattern is to store both: a UTC instant (ISO 8601) for deterministic round-tripping and ordering, plus the event's originating time-zone identifier for display intent and day grouping. UTC alone loses the answer to "what wall-clock time did the organizer mean?", and local time alone cannot be converted back to a correct instant once the user travels or DST rules change.
It is sufficient for instants — a concert start, a flight departure, anything that happens at one objective moment. Encode with ISO8601DateFormatter (or Codable's .iso8601 strategy) and you get a stable, sortable, server-friendly value.
It is not sufficient when the event carries wall-clock intent. "9 AM in Tokyo" must still render as 9 AM when the organizer's attendees view it in the event's context, and recurring events need the zone to recompute future instants after DST rule changes. So persist a pair:
struct Event: Codable {
var startsAt: Date // encoded as ISO8601 UTC instant
var timeZoneID: String // e.g. "Asia/Tokyo"
}At display time, configure a DateFormatter with timeZone = TimeZone(identifier: event.timeZoneID) for the venue view, or TimeZone.current for the "in my time" view. The stored instant never changes; only the presentation zone does.
Yes — derive the calendar's zone from the event, not from Calendar.current. Calendar.current silently tracks the device zone, so "events today" regroups when the user crosses a zone boundary or when DST shifts the local midnight. Build an explicit calendar:
func dayKey(for event: Event) -> Date {
var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: event.timeZoneID) ?? .current
return cal.startOfDay(for: event.startsAt)
}This keeps an event pinned to the same day in the venue's local reckoning regardless of where the device is. If your product instead defines "today" as the viewer's day, use TimeZone.current deliberately — the point is to make the choice explicit rather than inherited.
A conforming ISO 8601 string carries an offset or Z, so any formatter's default time zone is irrelevant when parsing it — this holds across iOS versions. The hazard is only with non-conforming strings that omit the offset (e.g. 2026-10-14T09:00:00). With NSDateFormatter, such strings are interpreted in the formatter's timeZone (defaulting to the device zone), producing off-by-hours instants that vary by device. ISO8601DateFormatter is stricter and version-stable; prefer it for anything crossing the wire, and reject or normalize offset-less legacy timestamps at ingestion rather than relying on defaults.
In a unit test: encode a known Date with ISO8601DateFormatter, decode it, and assert equality. Then compute startOfDay with the zone-pinned calendar above, change the simulator's time zone, and confirm the day key is unchanged while a Calendar.current-based key shifts. One caveat: exact behavior of offset-less ISO strings on pre-iOS 10 systems is based on historical reports, not re-verified here — if you still support legacy data from that era, test on the oldest OS you ship.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.