Fix: Convert an ISO 8601 String to `Date` in Swift

Fix: Convert an ISO 8601 String to `Date` in Swift

Written by

in

Quick answer: Use DateFormatter with the format yyyy-MM-dd'T'HH:mm:ssZ to parse this timestamp; set its locale to en_US_POSIX because the format is fixed. Then use Calendar.dateComponents to get the year, month, day, and hour, choosing UTC or the local time zone depending on which hour you need.

Why this happens

A string such as 2016-04-14T10:44:00+0000 is an ISO 8601 timestamp. The T is a literal separator between the date and time, and +0000 is the time-zone offset. It is not a format that can be parsed reliably by guessing from the user’s preferred date style; the parser needs a format that matches the input.

There is also an important difference between a Date and the date parts you see on screen. A Swift Date represents an instant in time, not a year-month-day-hour value in a particular time zone. The timestamp above represents 10:44 UTC. If you extract components using Calendar.current, the hour may differ because that calendar uses the device’s local time zone.

How to fix it

1. Parse the string with a matching DateFormatter format

For this exact input, use DateFormatter with a fixed format. The T must be quoted so the formatter treats it as a literal character. The final Z in the format pattern handles the numeric time-zone offset in the string, including +0000.

import Foundation

let input = "2016-04-14T10:44:00+0000"

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"

if let date = formatter.date(from: input) {
    print(date)
} else {
    print("Could not parse the timestamp")
}

date(from:) returns an optional. Keep the failure case rather than force-unwrapping the result: a missing field, unexpected separator, or different offset format can make parsing fail.

The POSIX locale is useful here because the input has a fixed, machine-readable format. It keeps parsing independent of a person’s language and regional date preferences. Do not substitute a localized date style for this fixed-format parsing task.

2. Extract the components in the time zone you mean

After parsing, ask a Calendar for the components. If you want the date and hour as represented in the timestamp’s UTC offset, use a UTC calendar:

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!

let components = calendar.dateComponents(
    [.year, .month, .day, .hour],
    from: date
)

print(components.year as Any)  // 2016
print(components.month as Any) // 4
print(components.day as Any)   // 14
print(components.hour as Any)  // 10

This produces hour 10 because the input offset is UTC. To get the hour in the user’s current time zone instead, use Calendar.current rather than setting the calendar to UTC:

let localComponents = Calendar.current.dateComponents(
    [.year, .month, .day, .hour],
    from: date
)

The local result can have a different day or hour near a time-zone boundary. Choose the time zone based on what your app means by “the hour”; do not assume that the timestamp’s written hour will always be the local hour.

3. Create a new date rounded down to the hour only if you need one

If you only need to display or store the extracted values, keep the DateComponents. You do not need to create another Date.

If another API specifically requires a Date with minutes and seconds removed, build it using the same calendar and time zone used for extraction. Set the smaller components explicitly:

var hourComponents = calendar.dateComponents(
    [.year, .month, .day, .hour],
    from: date
)
hourComponents.minute = 0
hourComponents.second = 0
hourComponents.nanosecond = 0

let roundedDownDate = calendar.date(from: hourComponents)

roundedDownDate is optional, so handle the possibility that the calendar cannot construct a date from those components. Use the matching calendar and time zone when you later read it; a Date itself does not retain a display time zone.

4. Use ISO8601DateFormatter when your input variants are standard ISO 8601

Foundation also provides ISO8601DateFormatter, which is designed for ISO 8601 date strings. It can be a good fit when your data uses a standard ISO 8601 representation rather than a single fixed layout:

import Foundation

let formatter = ISO8601DateFormatter()
let date = formatter.date(from: "2016-04-14T10:44:00+0000")

This also returns an optional. If you receive several variants—such as offsets with or without a colon—check that the formatter configuration accepts the formats in your data. When you know the exact incoming pattern, the explicit DateFormatter format in the first fix makes the expected layout clear.

How to prevent it

  • Match the format string to the input, including T, seconds, and the offset.
  • Use yyyy for the calendar year and MM for the month. mm means minutes; uppercase YYYY is a week-based year and can differ near New Year.
  • Set en_US_POSIX when parsing a fixed format with DateFormatter.
  • Decide whether date components should use UTC or the device’s local time zone.
  • Handle date(from:) and calendar.date(from:) as optional results.

FAQ

Why does the T need to be in single quotes?

In a DateFormatter pattern, quoted text is treated as a literal. Writing yyyy-MM-dd'T'HH:mm:ssZ tells the formatter that the input contains the letter T between its date and time fields.

Why is the hour different from the one in the string?

The string’s +0000 offset means UTC. Calendar.current extracts components in the device’s local time zone, which may show a different hour. Set the calendar’s time zone to UTC when you need the timestamp’s UTC components.

Should I use Date or DateComponents for just the year, month, day, and hour?

Use DateComponents if those are the values your app needs. Use a new Date only when another API requires an instant with the minutes and seconds removed, and construct it with the intended calendar and time zone.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.