How can I parse / create a date time stamp formatted with fractional seconds UTC timezone (ISO 8601, RFC 3339) in Swift?
Swift 4 • iOS 11.2.1 or later
extension ISO8601DateFormatter {
convenience init(_ formatOptions: Options) {
self.init()
self.formatOptions = formatOptions
}
}
extension Formatter {
static let iso8601withFractionalSeconds = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
}
extension Date {
var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
}
extension String {
var iso8601withFractionalSeconds: Date? { return Formatter.iso8601withFractionalSeconds.date(from: self) }
}
Usage:
Date().description(with: .current) // Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
let dateString = Date().iso8601withFractionalSeconds // "2019-02-06T00:35:01.746Z"
if let date = dateString.iso8601withFractionalSeconds {
date.description(with: .current) // "Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
print(date.iso8601withFractionalSeconds) // "2019-02-06T00:35:01.746Z\n"
}
iOS 9 • Swift 3 or later
extension Formatter {
static let iso8601withFractionalSeconds: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
}
Codable Protocol
If you need to encode and decode this format when working with Codable protocol you can create your own custom date encoding/decoding strategies:
extension JSONDecoder.DateDecodingStrategy {
static let iso8601withFractionalSeconds = custom {
let container = try $0.singleValueContainer()
let string = try container.decode(String.self)
guard let date = Formatter.iso8601withFractionalSeconds.date(from: string) else {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Invalid date: " + string)
}
return date
}
}
and the encoding strategy
extension JSONEncoder.DateEncodingStrategy {
static let iso8601withFractionalSeconds = custom {
var container = $1.singleValueContainer()
try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))
}
}
Playground Testing
let dates = [Date()] // ["Feb 8, 2019 at 9:48 PM"]
encoding
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
let data = try! encoder.encode(dates)
print(String(data: data, encoding: .utf8)!)
decoding
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
let decodedDates = try! decoder.decode([Date].self, from: data) // ["Feb 8, 2019 at 9:48 PM"]
Remember to set the locale to en_US_POSIX
as described in Technical Q&A1480. In Swift 3:
let date = Date()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
print(formatter.string(from: date))
The issue is that if you're on a device which is using a non-Gregorian calendar, the year will not conform to RFC3339/ISO8601 unless you specify the locale
as well as the timeZone
and dateFormat
string.
Or you can use ISO8601DateFormatter
to get you out of the weeds of setting locale
and timeZone
yourself:
let date = Date()
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds) // this is only available effective iOS 11 and macOS 10.13
print(formatter.string(from: date))
For Swift 2 rendition, see previous revision of this answer.
If you want to use the ISO8601DateFormatter()
with a date from a Rails 4+ JSON feed (and don't need millis of course), you need to set a few options on the formatter for it to work right otherwise the the date(from: string)
function will return nil. Here's what I'm using:
extension Date {
init(dateString:String) {
self = Date.iso8601Formatter.date(from: dateString)!
}
static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate,
.withTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime]
return formatter
}()
}
Here's the result of using the options verses not in a playground screenshot: