So konvertieren Sie eine Datumszeichenfolge mit optionalen Sekundenbruchteilen mithilfe von Codable in Swift4

Ich ersetze meinen alten JSON-Parsing-Code durch Swifts Codable und stoße in einen kleinen Haken. Ich denke, es ist nicht so sehr eine codierbare Frage als eine DateFormatter-Frage.

Beginne mit einer Struktur

struct JustADate: Codable { var date: Date } 

und eine JSON-Zeichenfolge

 let json = """ { "date": "2017-06-19T18:43:19Z" } """ 

Jetzt lässt sich dekodieren

 let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let data = json.data(using: .utf8)! let justADate = try! decoder.decode(JustADate.self, from: data) //all good 

Aber wenn wir das Datum ändern, so dass es Sekundenbruchteile hat, zum Beispiel:

 let json = """ { "date": "2017-06-19T18:43:19.532Z" } """ 

Jetzt bricht es. Die Daten kommen manchmal mit Sekundenbruchteilen zurück und manchmal nicht. Die Art, wie ich es getriggers habe, war in meinem Mapping-Code Ich hatte eine Transformationsfunktion, die beide DateFormats mit und ohne die Sekundenbruchteile ausprobierte. Ich bin mir nicht sicher, wie ich es mit Codable angehen soll. Irgendwelche Vorschläge?

    Sie können zwei verschiedene Datumsformatierer (mit und ohne Sekundenbruchteil) verwenden und eine benutzerdefinierte DateDecodingStrategy erstellen. Bei einem Fehler beim Analysieren des Datums, das von der API zurückgegeben wird, können Sie einen DecodingError auslösen, wie von @PauloMattos in Kommentaren vorgeschlagen:

    iOS 9, macOS 10.9, tvOS 9, watchOS 2, Xcode 9 oder höher

    Der benutzerdefinierte ISO8601 DateFormatter:

     extension Formatter { static let iso8601: 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 }() static let iso8601noFS: 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:ssXXXXX" return formatter }() } 

    Die benutzerdefinierte DateDecodingStrategy und der Error :

     extension JSONDecoder.DateDecodingStrategy { static let customISO8601 = custom { decoder throws -> Date in let container = try decoder.singleValueContainer() let string = try container.decode(String.self) if let date = Formatter.iso8601.date(from: string) ?? Formatter.iso8601noFS.date(from: string) { return date } throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)") } } 

    Die benutzerdefinierte DateEncodingStrategy :

     extension JSONEncoder.DateEncodingStrategy { static let customISO8601 = custom { date, encoder throws in var container = encoder.singleValueContainer() try container.encode(Formatter.iso8601.string(from: date)) } } 

    bearbeiten / aktualisieren :

    Xcode 9 • Swift 4 • iOS 11 oder höher

    ISO8601DateFormatter unterstützt nun formatOptions .withFractionalSeconds in iOS11 oder höher:

     extension Formatter { static let iso8601: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter }() static let iso8601noFS = ISO8601DateFormatter() } 

    Die Customs DateDecodingStrategy und DateEncodingStrategy wären dieselben wie oben gezeigt.


     // Playground testing struct ISODates: Codable { let dateWith9FS: Date let dateWith3FS: Date let dateWith2FS: Date let dateWithoutFS: Date } let isoDatesJSON = """ { "dateWith9FS": "2017-06-19T18:43:19.532123456Z", "dateWith3FS": "2017-06-19T18:43:19.532Z", "dateWith2FS": "2017-06-19T18:43:19.53Z", "dateWithoutFS": "2017-06-19T18:43:19Z", } """ let isoDatesData = Data(isoDatesJSON.utf8) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .customISO8601 do { let isoDates = try decoder.decode(ISODates.self, from: isoDatesData) print(Formatter.iso8601.string(from: isoDates.dateWith9FS)) // 2017-06-19T18:43:19.532Z print(Formatter.iso8601.string(from: isoDates.dateWith3FS)) // 2017-06-19T18:43:19.532Z print(Formatter.iso8601.string(from: isoDates.dateWith2FS)) // 2017-06-19T18:43:19.530Z print(Formatter.iso8601.string(from: isoDates.dateWithoutFS)) // 2017-06-19T18:43:19.000Z } catch { print(error) } 

    Alternativ zu @ Leos Antwort und wenn Sie Unterstützung für ältere Betriebssysteme ISO8601DateFormatter müssen ( ISO8601DateFormatter ist erst ab iOS 10, Mac OS 10.12 verfügbar), können Sie beim Analysieren der Zeichenfolge einen benutzerdefinierten Formatierer schreiben, der beide Formate verwendet:

     class MyISO8601Formatter: DateFormatter { static let formatters: [DateFormatter] = [ iso8601Formatter(withFractional: true), iso8601Formatter(withFractional: false) ] static func iso8601Formatter(withFractional fractional: Bool) -> 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\(fractional ? ".SSS" : "")XXXXX" return formatter } override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool { guard let date = (type(of: self).formatters.flatMap { $0.date(from: string) }).first else { error?.pointee = "Invalid ISO8601 date: \(string)" as NSString return false } obj?.pointee = date as NSDate return true } override public func string(for obj: Any?) -> String? { guard let date = obj as? Date else { return nil } return type(of: self).formatters.flatMap { $0.string(from: date) }.first } } 

    , die Sie als Datumsdecodierungsstrategie verwenden können:

     let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter()) 

    Obwohl dies bei der Implementierung ein wenig hässlicher ist, hat dies den Vorteil, dass es mit den Decodierungserrorsn konsistent ist, die Swift im Falle von errorshaften Daten wirft, da wir den Fehlerberichtsmechanismus nicht ändern.

    Beispielsweise:

     struct TestDate: Codable { let date: Date } // I don't advocate the forced unwrap, this is for demo purposes only let jsonString = "{\"date\":\"2017-06-19T18:43:19Z\"}" let jsonData = jsonString.data(using: .utf8)! let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter()) do { print(try decoder.decode(TestDate.self, from: jsonData)) } catch { print("Encountered error while decoding: \(error)") } 

    wird TestDate(date: 2017-06-19 18:43:19 +0000) drucken TestDate(date: 2017-06-19 18:43:19 +0000)

    Hinzufügen des Bruchteils

     let jsonString = "{\"date\":\"2017-06-19T18:43:19.123Z\"}" 

    führt zu der gleichen Ausgabe: TestDate(date: 2017-06-19 18:43:19 +0000)

    Allerdings mit einer falschen Zeichenfolge:

     let jsonString = "{\"date\":\"2017-06-19T18:43:19.123AAA\"}" 

    druckt den Standard-Swift-Fehler bei errorshaften Daten:

     Encountered error while decoding: dataCorrupted(Swift.DecodingError.Context(codingPath: [__lldb_expr_84.TestDate.(CodingKeys in _B178608BE4B4E04ECDB8BE2F689B7F4C).date], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))