La variable JSONDecoder
de Swift offre une propriété dateDecodingStrategy
qui permet de définir comment interpréter les chaînes de date entrantes conformément à un objet DateFormatter
.
Cependant, je travaille actuellement avec une API qui renvoie à la fois les chaînes de date (yyyy-MM-dd
) et les chaînes de date/heure (yyyy-MM-dd HH:mm:ss
), en fonction de la propriété. Existe-t-il un moyen de permettre à JSONDecoder
de gérer cela, étant donné que l'objet DateFormatter
fourni ne peut traiter qu'une seule dateFormat
à la fois?
Une solution simple consiste à réécrire les modèles Decodable
fournis pour accepter uniquement les chaînes en tant que propriétés et à fournir des variables publiques Date
getter/setter, mais cela me semble une mauvaise solution. Des pensées?
Il y a plusieurs façons de gérer cela:
DateFormatter
qui commence par essayer le format de chaîne date-heure, puis en cas d'échec, tente le format de date brut.custom
Date
dans laquelle vous demandez à Decoder
une singleValueContainer()
, décodez une chaîne et transmettez-la à tout formateur de votre choix avant de transmettre la date d'analyse.Date
qui fournit une init(from:)
et une encode(to:)
personnalisées qui le font (mais ce n'est vraiment pas mieux qu'une stratégie .custom
)init(from:)
personnalisée sur tous les types qui utilisent ces dates et tentent différentes opérations.Dans l’ensemble, les deux premières méthodes seront probablement les plus simples et les plus propres. Vous garderez l’implémentation par défaut synthétisée de Codable
partout sans sacrifier la sécurité de type.
Veuillez essayer un décodeur configuré de la même manière que ceci:
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
// possible date strings: "2016-05-01", "2016-07-04T17:37:21.119229Z", "2018-05-20T15:00:00Z"
let len = dateStr.count
var date: Date? = nil
if len == 10 {
date = dateNoTimeFormatter.date(from: dateStr)
} else if len == 20 {
date = isoDateFormatter.date(from: dateStr)
} else {
date = self.serverFullDateFormatter.date(from: dateStr)
}
guard let date_ = date else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateStr)")
}
print("DATE DECODER \(dateStr) to \(date_)")
return date_
})
return decoder
}()
Face à ce même problème, j'ai écrit l'extension suivante:
extension JSONDecoder.DateDecodingStrategy {
static func custom(_ formatterForKey: @escaping (CodingKey) throws -> DateFormatter?) -> JSONDecoder.DateDecodingStrategy {
return .custom({ (decoder) -> Date in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No Coding Path Found"))
}
guard let container = try? decoder.singleValueContainer(),
let text = try? container.decode(String.self) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date text"))
}
guard let dateFormatter = try formatterForKey(codingKey) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "No date formatter for date text")
}
if let date = dateFormatter.date(from: text) {
return date
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(text)")
}
})
}
}
Cette extension vous permet de créer une DateDecodingStrategy pour JSONDecoder qui gère plusieurs formats de date différents dans la même chaîne JSON. L'extension contient une fonction nécessitant l'implémentation d'une fermeture qui vous donne une Clé de code, et il vous appartient de fournir le DateFormatter correct pour la clé fournie.
Disons que vous avez le JSON suivant:
{
"publication_date": "2017-11-02",
"opening_date": "2017-11-03",
"date_updated": "2017-11-08 17:45:14"
}
La structure suivante:
struct ResponseDate: Codable {
var publicationDate: Date
var openingDate: Date?
var dateUpdated: Date
enum CodingKeys: String, CodingKey {
case publicationDate = "publication_date"
case openingDate = "opening_date"
case dateUpdated = "date_updated"
}
}
Ensuite, pour décoder le JSON, utilisez le code suivant:
let dateFormatterWithTime: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}()
let dateFormatterWithoutTime: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (key) -> DateFormatter? in
switch key {
case ResponseDate.CodingKeys.publicationDate, ResponseDate.CodingKeys.openingDate:
return dateFormatterWithoutTime
default:
return dateFormatterWithTime
}
})
let results = try? decoder.decode(ResponseDate.self, from: data)
essaye ça. (Swift 4)
var decoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
if let date = formatter.date(from: dateString) {
return date
}
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Cannot decode date string \(dateString)")
}
return decoder
}
Il n'y a aucun moyen de faire cela avec un seul encodeur. Dans ce cas, votre meilleur choix est de personnaliser les méthodes encode(to encoder:)
et init(from decoder:)
et de fournir votre propre traduction pour l’une de ces valeurs, en laissant la stratégie de date intégrée pour l’autre.
Il peut être intéressant d’examiner la possibilité de passer un ou plusieurs formateurs à l’objet userInfo
à cette fin.
Si vous avez plusieurs dates avec différents formats dans un même modèle, il est difficile d'appliquer .dateDecodingStrategy
pour chaque date.
Consultez ici https://Gist.github.com/romanroibu/089ec641757604bf78a390654c437cb0 pour une solution pratique
Il s’agit d’une approche quelque peu verbeuse, mais plus souple: encapsulez la date avec une autre classe Date et implémentez des méthodes de sérialisation personnalisées. Par exemple:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
class MyCustomDate: Codable {
var date: Date
required init?(_ date: Date?) {
if let date = date {
self.date = date
} else {
return nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let string = dateFormatter.string(from: date)
try container.encode(string)
}
required public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let raw = try container.decode(String.self)
if let date = dateFormatter.date(from: raw) {
self.date = date
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot parse date")
}
}
}
Alors maintenant, vous êtes indépendant de .dateDecodingStrategy
et .dateEncodingStrategy
et vos dates MyCustomDate
seront analysées avec le format spécifié. Utilisez-le en classe:
class User: Codable {
var dob: MyCustomDate
}
Instancier avec
user.dob = MyCustomDate(date)