Kitty
Kitty

Reputation: 21

Date string does not match format expected by formatter (SwiftData). Is it a problem with my @model and the codingKeys?

I am quite new with swiftUi and SwiftData. And also English is not my first language... I will try my best !

I am trying to get a date from a string using swiftData

This is the error that is giving me:

request failed dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "items", intValue: nil), _CodingKey(stringValue: "Index 8", intValue: 8), CodingKeys(stringValue: "volumeInfo", intValue: nil), CodingKeys(stringValue: "publishedDate", intValue: nil)], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))

I am following this tutorial (although it is not with swiftData)l: https://www.hackingwithswift.com/books/ios-swiftui/formatting-our-mission-view

This is how the date looks like in google books api: "publishedDate": "2016-02-07"

1A.-to get the date from a string, I have started adding this after let decoder = JSONDecoder() :

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)

1B.- Then (in the model) I have changed:

var publishedDate:String?

for

var publishedDate: Date?

and change it also in the init, the required init and in the encode function

So, it looks like this:

@Model
class VolumeInfo: Codable {

    enum CodingKeys: String, CodingKey {
        case title, subtitle, authors, publisher, publishedDate, formattedPublishedDate, summary = "description", pageCount, printType, averageRating, ratingsCount, categories, imageLinks, language
    }

    var title: String
    var subtitle: String?
    var authors: [String]?
    var publisher: String?
    var publishedDate: Date?
    var summary: String?
    var pageCount: Int?
    var printType: String?
    var averageRating: Float?
    var ratingsCount: Int?
    var categories: [String]?
    var imageLinks: ImageLinks?
    var language: String?

    init(
        title: String,
        subtitle: String? = nil,
        authors: [String]? = nil,
        publisher: String? = nil,
        publishedDate: Date? = Date.now,
        summary: String? = nil,
        pageCount: Int? = nil,
        printType: String? = nil,
        averageRating: Float? = nil,
        ratingsCount: Int? = nil,
        categories: [String]? = nil,
        imageLinks: ImageLinks? = nil,
        language: String? = nil
    ) {
        self.title = title
        self.subtitle = subtitle
        self.authors = authors
        self.publisher = publisher
        self.publishedDate = publishedDate
        self.summary = summary
        self.pageCount = pageCount
        self.printType = printType
        self.averageRating = averageRating
        self.ratingsCount = ratingsCount
        self.categories = categories
        self.imageLinks = imageLinks
        self.language = language
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
        authors = try container.decodeIfPresent([String].self, forKey: .authors)
        publisher = try container.decodeIfPresent(String.self, forKey: .publisher)
        publishedDate = try container.decodeIfPresent(Date.self, forKey: .publishedDate)
        summary = try container.decodeIfPresent(String.self, forKey: .summary)
        pageCount = try container.decodeIfPresent(Int.self, forKey: .pageCount)
        printType = try container.decodeIfPresent(String.self, forKey: .printType)
        averageRating = try container.decodeIfPresent(Float.self, forKey: .averageRating)
        ratingsCount = try container.decodeIfPresent(Int.self, forKey: .ratingsCount)
        categories = try container.decodeIfPresent([String].self, forKey: .categories)
        imageLinks = try container.decodeIfPresent(ImageLinks.self, forKey: .imageLinks)
        language = try container.decodeIfPresent(String.self, forKey: .language)
    }

    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encodeIfPresent(subtitle, forKey: .subtitle)
        try container.encodeIfPresent(authors, forKey: .authors)
        try container.encodeIfPresent(publisher, forKey: .publisher)
        try container.encodeIfPresent(publishedDate, forKey: .publishedDate)
        try container.encodeIfPresent(summary, forKey: .summary)
        try container.encodeIfPresent(pageCount, forKey: .pageCount)
        try container.encodeIfPresent(printType, forKey: .printType)
        try container.encodeIfPresent(averageRating, forKey: .averageRating)
        try container.encodeIfPresent(ratingsCount, forKey: .ratingsCount)
        try container.encodeIfPresent(categories, forKey: .categories)
        try container.encodeIfPresent(imageLinks, forKey: .imageLinks)
        try container.encodeIfPresent(language, forKey: .language)
    }
}

2.- To get the string from the date and to be able to use it in my views, I have added a computed property to my model (as in Paul's example)l, adding it to my enum CodingKeys and to the func decode

var formattedPublishedDate: String {
        publishedDate?.formatted(date: .abbreviated, time: .omitted) ?? "N/A"
    }

So, the result is this:

@Model
class VolumeInfo: Codable {

    enum CodingKeys: String, CodingKey {
        case title, subtitle, authors, publisher, publishedDate, formattedPublishedDate, summary = "description", pageCount, printType, averageRating, ratingsCount, categories, imageLinks, language
    }

    var title: String
    var subtitle: String?
    var authors: [String]?
    var publisher: String?
    var publishedDate: Date?

    var formattedPublishedDate: String {
        publishedDate?.formatted(date: .abbreviated, time: .omitted) ?? "N/A"
    }

    var summary: String?
    var pageCount: Int?
    var printType: String?
    var averageRating: Float?
    var ratingsCount: Int?
    var categories: [String]?
    var imageLinks: ImageLinks?
    var language: String?

    init(
        title: String,
        subtitle: String? = nil,
        authors: [String]? = nil,
        publisher: String? = nil,
        publishedDate: Date? = Date.now,
        summary: String? = nil,
        pageCount: Int? = nil,
        printType: String? = nil,
        averageRating: Float? = nil,
        ratingsCount: Int? = nil,
        categories: [String]? = nil,
        imageLinks: ImageLinks? = nil,
        language: String? = nil
    ) {
        self.title = title
        self.subtitle = subtitle
        self.authors = authors
        self.publisher = publisher
        self.publishedDate = publishedDate
        self.summary = summary
        self.pageCount = pageCount
        self.printType = printType
        self.averageRating = averageRating
        self.ratingsCount = ratingsCount
        self.categories = categories
        self.imageLinks = imageLinks
        self.language = language
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
        authors = try container.decodeIfPresent([String].self, forKey: .authors)
        publisher = try container.decodeIfPresent(String.self, forKey: .publisher)
        publishedDate = try container.decodeIfPresent(Date.self, forKey: .publishedDate)
        summary = try container.decodeIfPresent(String.self, forKey: .summary)
        pageCount = try container.decodeIfPresent(Int.self, forKey: .pageCount)
        printType = try container.decodeIfPresent(String.self, forKey: .printType)
        averageRating = try container.decodeIfPresent(Float.self, forKey: .averageRating)
        ratingsCount = try container.decodeIfPresent(Int.self, forKey: .ratingsCount)
        categories = try container.decodeIfPresent([String].self, forKey: .categories)
        imageLinks = try container.decodeIfPresent(ImageLinks.self, forKey: .imageLinks)
        language = try container.decodeIfPresent(String.self, forKey: .language)
    }

    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encodeIfPresent(subtitle, forKey: .subtitle)
        try container.encodeIfPresent(authors, forKey: .authors)
        try container.encodeIfPresent(publisher, forKey: .publisher)
        try container.encodeIfPresent(publishedDate, forKey: .publishedDate)
        try container.encodeIfPresent(formattedPublishedDate, forKey: .formattedPublishedDate)
        try container.encodeIfPresent(summary, forKey: .summary)
        try container.encodeIfPresent(pageCount, forKey: .pageCount)
        try container.encodeIfPresent(printType, forKey: .printType)
        try container.encodeIfPresent(averageRating, forKey: .averageRating)
        try container.encodeIfPresent(ratingsCount, forKey: .ratingsCount)
        try container.encodeIfPresent(categories, forKey: .categories)
        try container.encodeIfPresent(imageLinks, forKey: .imageLinks)
        try container.encodeIfPresent(language, forKey: .language)
    }
}

this the first time that I try to use a date from a string using swiftData... I am quite new learning swiftData...

Can someone please help me? how could I solve that error?

Upvotes: 2

Views: 123

Answers (1)

It seems that the Google dates data is not very consistent. As mentioned, in my tests I came across these variations: "publishedDate": "2015-09" and "2021-2-28".

So to cater for multiple date formats, you could try using this decoding approach where you test the different date formats (parseDate) as shown in this example code:

@Model
class VolumeInfo: Codable {

    // ...

    init(...)
    
    func parseDate(from string: String) -> Date? {
        let formats = ["yyyy-M-d", "yyyy-M", "yyyy-MM-d", "yyyy-MM", "yyyy-MM-dd"]
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        for format in formats {
            dateFormatter.dateFormat = format
            if let date = dateFormatter.date(from: string) {
                return date
            }
        }
        return nil
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        // ...
        publishedDate = nil
        if let dateString = try container.decodeIfPresent(String.self, forKey: .publishedDate) {
            publishedDate = parseDate(from: dateString)
        }
        // ...
    }
    // ...
}

Upvotes: 1

Related Questions