How to parse ISO 8601 date in Swift

⋅ 6 min read ⋅ Swift DateFormatter Date

Table of Contents

What is ISO 8601

Date representation is complicated. Everyone has their own interpretation of the date. For example, 01/05/12 could mean January 5, 2012, or May 1, 2012.

This can be problematic if you want to communicate date and time information. How can we ensure that the receiver interprets date time the same way as a sender?

ISO 8601 tackles this problem by setting out an international standard way to represent dates. It has many variations to support date, date/time, with and without timezone and many more. Here are some examples:

  • Date: 2022-01-31
  • Date and time in UTC: 2022-01-31T02:22:40+00:00
  • Date and time in UTC: 2022-01-31T02:22:40Z

You might have seen this kind of a date representation in some API responses, which makes perfect sense since we want both parties to agree on the same date format.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

How to parse dates

When parsing a date from a string, you might think of DateFormatter.

You have to manually set dateFormat and locale to DateFormatter instance.

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
let date = formatter.date(from: "2022-01-31T02:22:40Z")

You probably need this formatter every time you need to talk with API. Luckily, since iOS 10, Apple introduced a dedicated formatter for handling ISO 8601 date parsing, ISO8601DateFormatter.

ISO8601DateFormatter

ISO8601DateFormatter is simpler than DateFormatter. We don't have to set locale of dateFormat. Less code means less chance we can do wrong with this new formatter.

Here is an example how we parsing ISO 8601 date string with ISO8601DateFormatter. What we need to do is initialize and use it.

let newFormatter = ISO8601DateFormatter()
let date = newFormatter.date(from: "2022-01-31T02:22:40Z")

Caveats

ISO 8601 is a way to represent date/time that means it can be presented in a number of ways, e.g., without time, without a timezone, without dash separator in date, etc.

Here are some variations.

  • 2022-01-31
  • 2022-01-31T02:22:40+00:00
  • 2022-01-31T02:22:40Z
  • 20220131T022240Z

What you might not be aware of is ISO8601DateFormatter won't handle all of these variations out of the box.

As you can see in the following example, ISO8601DateFormatter failed to parse "2022-01-31" which result in nil.

let formatter = ISO8601DateFormatter()
let date = formatter.date(from: "2022-01-31")
print(date) // nil

We control the variation of date format by setting a combination of ISO8601DateFormatter.Options, which we can set via the formatOptions property.

The default formatOptions is .withInternetDateTime which is the format aim for use on the Internet defined in RFC 3339.

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]

I can't go through every combination of format options, but I can show some popular ones.

ISO 8601 With fractional seconds

We can support the date with milliseconds part by specifying .withFractionalSeconds. We can do this by setting whole new options to formatOptions or inserting .withFractionalSeconds into the current format.

let formatter = ISO8601DateFormatter()
// Insert .withFractionalSeconds to the current format.
formatter.formatOptions.insert(.withFractionalSeconds)
let date = formatter.date(from: "2022-01-05T03:30:00.000Z")

// Set new format with .withFractionalSeconds
formatter.formatOptions = [
.withInternetDateTime,
.withFractionalSeconds
]
let date2 = formatter.date(from: "2022-01-05T03:30:00.000Z")

ISO 8601 without dash and colon

ISO 8601 also supports date/time without separator, e.g., 20220131T022240Z (2022-01-31T02:22:40Z). To parse this format, we need to specify each element one by one.

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [
.withDay,
.withMonth,
.withYear,
.withTime,
.withTimeZone
]

let date = formatter.date(from: "20220131T022240Z")
print(date)
// 2022-01-31 02:22:40 +0000

ISO8601DateFormatter.Options

Here are some examples of how it looks for various combinations of format options.

The following table shows the various alignment you can set and how it would position.

Format Example Options
Date 2022-01-31 withFullDate
Day, Month, Year 20220131 withDay, withMonth, withYear
Date and Time 2022-01-31T08:42:07Z withFullDate, withFullTime
Date and Time with space separator between date and time 2022-01-31 08:42:07Z withFullDate, withFullTime, withSpaceBetweenDateAndTime
Year and Week of Year 2022-W05 withYear, withWeekOfYear, withDashSeparatorInDate
Year, Week of Year with Ordinal Weekday 2022-W05-01 withYear, withWeekOfYear, withDay, withDashSeparatorInDate

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Codable

We can set the date encoding strategy to JSONEncoder/JSONDecoder to control how you want a date to be decoded and encoded. If the date string is in RFC 3339 format, you can use a built-in .iso8601 date coding strategy.

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

If you have a date other than RFC 3339 format, you must specify a .custom date coding strategy.

Since ISO8601DateFormatter doesn't inherit from DateFormatter, you can't set it with the .formatted date coding strategy since it accepts DateFormatter.

public enum DateDecodingStrategy {
/// Decode the `Date` as a string parsed by the given formatter.
case formatted(DateFormatter)
...
}

We can't see it like this:

let encoder = JSONEncoder()

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]

encoder.dateEncodingStrategy = .formatted(formatter)
// Cannot convert value of type 'ISO8601DateFormatter' to expected argument type 'DateFormatter'

Instead, we need to use the .custom date coding strategy.

Encoder

Here is how you set custom ISO 8601 date format for JSONEncoder.

let encoder = JSONEncoder()

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]

encoder.dateEncodingStrategy = .custom({ date, encoder in
var container = encoder.singleValueContainer()
let dateString = formatter.string(from: date)
try container.encode(dateString)
})

Decoder

Here is how you set custom ISO 8601 date format for JSONDecoder.

let decoder = JSONDecoder()

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]

decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)

if let date = formatter.date(from: dateString) {
return date
}

throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
})

  1. https://developer.apple.com/documentation/foundation/iso8601dateformatter/1643324-formatoptions ↩︎


Read more article about Swift, DateFormatter, Date, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
How to reset push notification permission on iOS

Learn how to make notification permission dialog popup again like the first time you run the app.

Next
How to render text with a color gradient in SwiftUI

Learn how to apply gradient colors to a SwiftUI text view.

← Home