Skip to content

Instantly share code, notes, and snippets.

@weyhan
Created September 25, 2022 14:01
Show Gist options
  • Save weyhan/c71cb5ca46eab40a68605832525d259f to your computer and use it in GitHub Desktop.
Save weyhan/c71cb5ca46eab40a68605832525d259f to your computer and use it in GitHub Desktop.
Static ISO8601DateFormatter in Swift

Static ISO8601DateFormatter in Swift

Caveat

I've never tested or verified that the solution presented here is correct or works to achieve avoiding the scenario as described in the Apple Developer Documentation.

All comments and criticism are welcome.

Background

In my recent work, I've came across a situation that I need to parse a variant of the ISO8601 date-time format. The date-time strings are from a new set of REST APIs that the app consumes in the implementation of a new feature. As the new feature needs, these date-times from the APIs needs to be in Swift Date to be useful because there will be date comparison (for sorting, searching, and etc). Additionally, for display, these dates need to be transformed to the local format or the display simply need the date part or for some other instance, need only the time part. These String date to Date conversion is either going to be at the point of the APIs are called or at the point of using them. In either case, they are happening in multiple locations in the project.

The problem is that the date format received from the APIs are not supported out of the box with the ISO8601DateFormatter. These date-times came as local time and in the format yyyy-MM-dd'T'HH:mm:ss (without the time zone) instead of yyyy-MM-dd'T'HH:mm:ssXXXXX. The frustrating part is that the ISO8601DateFormatter does not follow the ISO8601 standard where if the date-time does not have the time zone information, local time is assumed but the formatter assumes UTC time zone instead. Setting the time zone for the formatter does not change the result either. In order to parse the date string successfully, a custom setting need to be set on the formatOptions property in addition to setting the time zone for the formatter. This is when I saw the warning in Xcode Developer Documentation.

Important

Resetting this property can incur a significant performance cost, as it may cause internal state to be regenerated.

Here is the link to the ISO8601DateFormatter documentation on the Apple's Developer portal

Initially I thought the simple solution would be to create an instance of the ISO8601DateFormatter and setup it up once and make sure that the options is not touch ever again in the session. Even if the project is only touched by one developer, there is no guarantee it will stay that way, not to mention a project that is worked on by multiple developer.

In the end, I've created a class that is only meant to be used as a static object and never instantiated during used. See the Swift file in this gist for the implementation details.

//
// DateTime.swift
// HLACore
//
// Created by WeyHan Ng on 24/09/2022.
//
import Foundation
// Input format is the format of the date string being converted to Swift Date.
public enum InputFormat {
case isoDate
case isoDateTime
case isoDateTimeWithoutTimeZone
}
// Output format is the date string format used to convert Swift Date to string.
public enum OutputFormat {
case isoDate
case isoDateTime
case isoDateTimeWithoutTimeZone
case localDate
case localDateTimeWithoutSeconds
case localTime
}
//
// Usage:
// To convert "yyyy-MM-dd'T'HH:mm:ssXXXXX" formatted string to Swift Date.
// TimeZone ("XXXXX") can be the special zone "Z" meaning 0 offset from UTC or
// ("[+-]99:99") plus or minus sign followed by 2 digit hours followed by a
// colon followed by 2 digit minutes offset from UTC. .e.g.:
//
// let date1 = StaticDateFormatter.date(from: "2022-09-24T10:36:27-05:00")
// let date2 = StaticDateFormatter.date(from: "2022-09-24T15:36:27Z")
//
// Note: The above date1 and date2 are the same moment.
//
// To convert "yyyy-MM-dd'T'HH:mm:ss" formatted string to Swift Date.
// TimeZone is not given in the date-time string but otherwise the same as the
// above example. e.g.:
//
// let date3 = StaticDateFormatter.date(from: "2022-09-24T15:36:27",
// format: .isoDateTimeWithoutTimeZone)
//
// The above assumes local time zone and is hardcoded in StaticDateFormatter.
// Change the local time zone as required for your own usage.
//
// To convert Swift Date to string using the default date-time format.
// To use the default date-time format, the `format` argument can be left out.
// e.g.:
//
// let dateStr1 = StaticDateFormatter.string(from: Date())
//
// To convert Swift Date to a specific date-time format.
//
// let dateStr2 = StaticDateFormatter.string(from: Date(),
// format: .localDateTimeWithoutSeconds)
//
// The output format hardcoded here to avoid typo in code. Only the enums
// associated with the hardcoded format is expose and used in the project to
// select the formats used for the conversion.
//
public class StaticDateFormatter {
// Add or remove the static formatter as required to tailor for your specific
// needs. Make the same changes to the InputFormat and OutputFormat enum as
// required.
private static let isoDateFormatter = ISO8601DateFormatter()
private static let isoDateTimeFormatter = ISO8601DateFormatter()
private static let isoDateTimeNoTimeZoneFormatter = ISO8601DateFormatter()
private static let localDateFormatter = DateFormatter()
private static let localDateTimeLongFormatter = DateFormatter()
private static let localTimeFormatter = DateFormatter()
private static var notSetup = true
// Hide the initializer because this class is meant to be used as static object
// and should never be instantiated.
private init() { }
public static func string(from date: Date, format: OutputFormat = .localDate) -> String {
setupIfNeeded()
switch format {
case .isoDate:
return isoDateFormatter.string(from: date)
case .isoDateTime:
return isoDateTimeNoTimeZoneFormatter.string(from: date)
case .isoDateTimeWithoutTimeZone:
return isoDateTimeNoTimeZoneFormatter.string(from: date)
case .localDate:
return localDateFormatter.string(from: date)
case .localDateTimeWithoutSeconds:
return localDateTimeLongFormatter.string(from: date)
case .localTime:
return localTimeFormatter.string(from: date)
}
}
// Change the default for InputFormat to the most common uses in your project
// or remove the defaults to force explicit choice at the calling side.
public static func date(from string: String, format: InputFormat = .isoDateTimeWithoutTimeZone) -> Date? {
setupIfNeeded()
switch format {
case .isoDate:
return isoDateFormatter.date(from: string)
case .isoDateTime:
return isoDateTimeFormatter.date(from: string)
case .isoDateTimeWithoutTimeZone:
return isoDateTimeNoTimeZoneFormatter.date(from: string)
}
}
private static func setupIfNeeded() {
// Only setup formatter once per session.
guard notSetup == true else {
return
}
// Change the time zone to reflect your own usage.
let localTimeZone = TimeZone(identifier: "America/Los_Angeles")
// Formatter to parse ISO8601 date string. i.e. "yyyy-MM-dd"
isoDateFormatter.timeZone = localTimeZone
isoDateFormatter.formatOptions = [
.withFullDate,
.withDashSeparatorInDate
]
// Formatter to parse ISO8601 date-time string. i.e. "yyyy-MM-dd'T'HH:mm:ssXXXXX"
// where XXXXX is the time zone offset from UTC
isoDateTimeFormatter.timeZone = localTimeZone
isoDateTimeFormatter.formatOptions = [
.withFullDate,
.withTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime,
.withTimeZone,
.withColonSeparatorInTimeZone
]
// Formatter to parse ISO8601 datetime string that does not specify the
// timezone. i.e. "yyyy-MM-dd'T'HH:mm:ss"
isoDateTimeNoTimeZoneFormatter.timeZone = localTimeZone
isoDateTimeNoTimeZoneFormatter.formatOptions = [
.withFullDate,
.withTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime
]
// Formatter to convert Date to US date string format.
localDateFormatter.timeZone = localTimeZone
localDateFormatter.dateFormat = "MM-dd-yyyy"
// Formatter to convert Date to Malaysia date-time string format.
localDateTimeLongFormatter.timeZone = localTimeZone
localDateTimeLongFormatter.dateFormat = "MM-dd-yyyy hh:mm a"
// Formatter to convert Date to Malaysia time string format.
localTimeFormatter.timeZone = localTimeZone
localTimeFormatter.dateFormat = "hh:mm a"
// Turn off setup
notSetup = false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment