Converting timestamp from Firebase and adding to array - swift

I'm trying to read a timeStamp from Firebase and append it to an array.
I have made some progress:
var orderDateHistoryArray = [String:Int]()
func getOrderDates() {
let uid = Auth.auth().currentUser!.uid
let orderDateHistoryRef = Database.database().reference().child("users/\(uid)/Orders/")
orderDateHistoryRef.observeSingleEvent(of: .value, with: { (snapshot) in
// Get dates
let value = snapshot.value as? NSDictionary
if let orderDate = value?["Date"] as? [String:Int] {
self.orderDateHistoryArray += Array(orderDate.values)//This does not conform
print(orderDate)
}
self.tableView.reloadData()
// ...
}) { (error) in
print(error.localizedDescription)
}
}
The print(orderDate)statement prints:
["-LQYspEghK3KE27MlFNE": 1541421618601,
"-LQsYbhf-vl-NnRLTHhK": 1541768379422,
"-LQYDWAKlzTrlTtO1Qiz": 1541410526186,
"-LQsILjpNqKwLl9XBcQm": 1541764115618]
This is childByAutoID : timeInMilliseconds
So, I want to read out the timeInMilliseconds, convert it to a readable timestampand append it to the orderDateHistoryArray

Those are timestamps. Parse it as a Date object then use .toLocaleDateString to get date.
alert( new Date(1541421618601).toLocaleDateString("en-US") );

In order to transform your timestamp you must, first remove milliseconds on each the values returned by the dictionary.
self.orderDateHistoryArray += Array(orderDate.values).map { Date(timeIntervalSince1970: TimeInterval($0/1000)) }
In order to get it in a "human way", you need to have a DateFormatter. It's on this object where you define how it's presented.
extension Date {
func format(_ dateFormat: String = "dd/MMMM")
let formatter = DateFormatter()
formatter.timeZone = TimeZone.current
formatter.dateFormat = "MMMM dd"
return formatter.string(from: self)
}
}
and on a Date element you can just call it by date.format() or by passing a string date.format("yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX")

Related

How to read a Firestore timestamp in Swift

I'm trying to read a timestamp from my Firestore database and ultimately convert it to a date, but for some reason my code doesn't seem to be returning anything. Instead, it only seems to use the default value that I provide, which is 0, so it always reads as Jan 1, 1970.
The document I'm trying to read in Firestore includes a field called date, which has a type of timestamp. There are other fields in the document, but to keep things simple I've left those out from this question. For reference, the other fields from the document are successfully read.
I've tried the below code. Note that I have imported Firebase to the class:
surveyDataCollectionRef.whereField("uid", isEqualTo: Auth.auth().currentUser?.uid ?? "").getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
} else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let timestamp = data["date"] as? TimeInterval ?? 0
let date = Date(timeIntervalSince1970: timestamp)
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
dateFormatter.locale = NSLocale.current
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
let strDate = dateFormatter.string(from: date)
let newSurvey = Survey(date: strDate)
self.surveys.append(newSurvey!)
self.currentSurveys = self.surveys
}
self.tableView.reloadData()
}
}
}
After even more trial and error I found that the below code seemed to be able to get me a date as a String value (and in the correct format that I was aiming for:
if let timestamp = data["date"] as? Timestamp {
let date = timestamp.dateValue()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
strDate = "\(dateFormatter.string(from: date))"
}

How can I filter by specific date from realm object in swift?

I have a realm object with date property type of Date , and want to get list of items with specific date.
If I click specific date from calendar, for example 2020-03-06 , then it will present list of items which was created in 2020-03-06.
:: EDITED ::
Here is my realm object named "Profile" and there are dates from
2020-03-05 to 2020-03-08 .
Here is my Profile object and ProfileManager Singleton.
class Profile: Object {
#objc dynamic var date: Date!
#objc dynamic var content: String!
convenience init(_ content: String) {
self.init()
self.content = content
self.date = Date()
}
}
class ProfileManager {
static let shared = ProfileManager()
private var realm = try! Realm()
var profileList: Results<Profile>?
private init() {
profileList = realm.objects(Profile.self)
}
func save(_ object: Profile) {
do {
try realm.write {
realm.add(object)
}
} catch {
print(error)
}
}
func addNewProfile(_ content: String) {
let newProfile = Profile(content)
save(newProfile)
}
}
And lastly, here is a viewController which has to buttons. One for
adding new Profile, and one for printing filtered profile list.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addProfilePressed(_ sender: Any) {
ProfileManager.shared.addNewProfile("profile content")
}
#IBAction func filterButtonPressed(_ sender: Any) {
let stringDate = "2020-03-09"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let searchDate:Date = dateFormatter.date(from: stringDate)!
let results = ProfileManager.shared.profileList!.filter("date == %#", searchDate)
print(searchDate)
print(results)
for profile in results {
print(profile.content!)
}
}
}
the result on the console, when filterButtonPressed method called.
2020-03-08 15:00:00 +0000
Results<Profile> <0x7f9b36f160a0> (
)
How can I fix this problem?
And here is another problem.
I set to 'stringDate' a value of "2020-03-09"
but when I print converted date 'searchDate' , it prints "2020-03-08"
why this happens?
Hope now my questions is more clear to understand.
My original answer is below which, after a lot of research was only somewhat correct.
The actual answer has to do with the timestamp portion of the date.
So... if we create a date object using the below code and set it to a known date,
let stringDate = "2020-03-08"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let searchDate:Date = dateFormatter.date(from: stringDate)!
the actual object will look like this
2020-03-08T05:00:00.000Z
However, how the Profile object is being created is like this
convenience init(_ content: String) {
self.init()
self.content = content
self.date = Date()
}
and that date object looks like this
2020-03-08T16:10:25.123Z
so as you can see, if we filter for a specific date these are not equal
2020-03-08T05:00:00.000Z != 2020-03-08T16:10:25.123Z
which is why this
let stringDate = "2020-03-08"
let searchDate:Date = dateFormatter.date(from: stringDate)!
let searchResults = realm.objects(Profile.self).filter("date == %#", searchDate)
could not find the date because it's filtering for this
2020-03-08T05:00:00.000Z
To fix, change the profile class with a date stamp with a default time stamp
class Profile: Object {
#objc dynamic var date: Date!
#objc dynamic var content: String!
convenience init(_ content: String) {
self.init()
self.content = content
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.dateFormat = "MM/dd/yy"
let today = Date()
let s = formatter.string(from: today)
let d = formatter.date(from: s)
self.date = d
}
}
or, store your dates as a string yyyymmdd which will remove the ambiguity completely.
-- ORIGINAL ANSWER BELOW ---
Filtering by date is fully supported on date objects. Here's two quick examples. One for filtering for a specific date (for your question) and one for a date range using BETWEEN.
Note, I have a function makeDate that casts a string to a date object. This example uses a Realm DogClass object that has a dog_birthdate Date property.
This filters for objects with a specific date
let searchDate = self.makeDate(fromString: "06/01/2019")
let specificDateResults = realm.objects(DogClass.self)
.filter("dog_birthdate == %#", searchDate)
for dog in specificDateResults {
print(dog.dog_name)
}
This filters for objects within a date range
let startDate = self.makeDate(fromString: "06/01/2019")
let endDate = self.makeDate(fromString: "06/20/2019")
let dateRangeResuls = realm.objects(DogClass.self)
.filter("dog_birthdate BETWEEN {%#,%#}", startDate, endDate)
for dog in dateRangeResuls {
print(dog.dog_name)
}
EDIT: Using the code in the comment from the OP for testing
let stringDate = "2019-06-01"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let searchDate:Date = dateFormatter.date(from: stringDate)!
let result = realm.objects(DogClass.self).filter("dog_birthdate == %#", searchDate)
for dog in result {
print(dog.dog_name)
}
which works perfectly.

How to store date in userdefaults?

I'd like to record the history of her entry into the textfield. I intend to register this with UserDefaults. But when I try to save it with UserDefaults, "cannot assign value of type 'nsdate'?'to type 'String' " Error. I don't think it's accepting textfield data because it's string. And how can I keep history in memory?
formatteddate and formatteddate2 give this error. The date output is as follows : 20/04/2019 20:23
let token = try? keychain.getString("chipnumbernew")
chip1InfoString = token
var time = NSDate()
var formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy HH:mm"
var formatteddate = formatter.string(from: time as Date)
var formatteddate2 = formatter.string(from: time as Date)
timertextNew.text = formatteddate
timertext2New.text = formatteddate2
let RetrivedDate = UserDefaults.standard.object(forKey: "timertext") as? NSDate
formatteddate = RetrivedDate
let RetrivedDate2 = UserDefaults.standard.object(forKey: "timertext2") as? NSDate
formatteddate2 = RetrivedDate2
If you only want to display the date value you can convert and store it as string otherwise you convert/format it after you have read it, either way you should make sure you use the same type when saving and reading
//save as Date
UserDefaults.standard.set(Date(), forKey: key)
//read
let date = UserDefaults.standard.object(forKey: key) as! Date
let df = DateFormatter()
df.dateFormat = "dd/MM/yyyy HH:mm"
print(df.string(from: date))
// save as String
let df = DateFormatter()
df.dateFormat = "dd/MM/yyyy HH:mm"
let str = df.string(from: Date())
UserDefaults.standard.setValue(str, forKey: key)
// read
if let strOut = UserDefaults.standard.string(forKey: key) {
print(strOut)
}
The following saves a Date object as a Double (a.k.a. TimeInterval). This avoids any date formatting. Formatting can lose precision, and is unnecessary since the string is not intended for users to read.
// save
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: key)
// read
let date = Date(timeIntervalSince1970: UserDefaults.standard.double(forKey: key))
you can always create an extension for saving and retrieving the date in userdefault. here the example code:
import Foundation
extension UserDefaults {
func set(date: Date?, forKey key: String){
self.set(date, forKey: key)
}
func date(forKey key: String) -> Date? {
return self.value(forKey: key) as? Date
}
}
let userDefault = UserDefaults.standard
userDefault.set(date: Date(), forKey: "timertext")
print(userDefault.date(forKey: "timertext") ?? "")
Your problem is that you are retrieving NSDate from the user default storage and then trying to assign them to a String (formatteddate).
Try this;
formatteddate = formatter.string(from: RetrivedDate as Date)

Group Items By Property in Swift 4

My data source is
{"events": [{"name":"event
foo","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test
"}, {"name":"event
bar","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test2"},
{"name":"event
foobar","date":"2018-07-21","time":"11:00","am_or_pm":"PM","day":"Saturday","description":"test3"}]}
I have tried dictionary/arrays, but not really getting close to my wanted result.
Pulling out data into an array:
var times = ["9:00","9:00","11:00"]
var names = ["event foo","event bar","event foobar"]
Desired output:
["9:00", "11:00"]
[["event foo", "event bar"], ["event foobar"]]
Any pointers to do this in Swift is appreciated. My end result is to hope to section a uitableview grouped by time.
If using Swift 4, you can use reduce(into:) and the default value for subscript operator:
guard
let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: [Any]],
let events = json["events"] as? [[String: String]] else {
return
}
let results = events.reduce(into: [String: [String]]()) { result, value in
guard let time = value["time"], let name = value["name"] else { return }
result[time, default: []].append(name)
}
That results in a dictionary:
["11:00": ["event foobar"], "7:00": ["event foo", "event bar"]]
Or, as Vadian suggested, you can use Dictionary(grouping:,by:), but then you have to map it if you only want your name values, resulting in an array of tuples:
let results = Dictionary(grouping: events, by: { $0["time"]! })
.map { ($0.key, $0.value.map { $0["name"]! })}
[("11:00", ["event foobar"]), ("7:00", ["event foo", "event bar"])]
Personally, like Vadian suggested, I'd be inclined to combine date, time, and am_or_pm to build a full Date object and use one of the above patterns. E.g.:
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd h:mm a"
formatter.locale = Locale(identifier: "en_US_POSIX")
// if date/time values are in GMT, uncomment the following line:
//
// formatter.timeZone = TimeZone(secondsFromGMT: 0)
let results = events.reduce(into: [Date: [String]]()) { result, value in
guard
let timeString = value["time"],
let dateString = value["date"],
let amPm = value["am_or_pm"],
let date = formatter.date(from: dateString + " " + timeString + " " + amPm),
let name = value["name"] else { return }
result[date, default: []].append(name)
}
or
let results = Dictionary(grouping: events, by: { dictionary -> Date in
let string = dictionary["date"]! + " " + dictionary["time"]! + " " + dictionary["am_or_pm"]!
return formatter.date(from: string)!
})
.map { ($0.key, $0.value.map { $0["name"]! })}
Or, if the web service returned a single ISO 8601/RFC 3339 string representation of the date, time, and am/pm in the JSON, this could be simplified further.
I recommend to decode the JSON with Decodable and create a full date from the components. Then use Dictionary(grouping:by:) to group the array.
First create a DateFormatter (uncomment the time zone line if you need absolute UTC dates)
let dateFormatter : DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
// formatter.timeZone = TimeZone(secondsFromGMT: 0)!
formatter.dateFormat = "yyyy-MM-dd hh:mm a"
return formatter
}()
Create two structs for the root element and the events array. A custom initializer creates the Date instance
struct Root : Decodable {
let events : [Event]
}
struct Event : Decodable {
let date : Date
let name, description : String
private enum CodingKeys: String, CodingKey { case name, date, time, am_or_pm, description}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
description = try container.decode(String.self, forKey: .description)
let datePortion = try container.decode(String.self, forKey: .date)
let timePortion = try container.decode(String.self, forKey: .time)
let ampm = try container.decode(String.self, forKey: .am_or_pm)
let dateString = "\(datePortion) \(timePortion) \(ampm)"
guard let fullDate = dateFormatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(forKey: .date,
in: container,
debugDescription: "Date cannot be created")
}
date = fullDate
}
}
Decode the JSON and group the array
let jsonString = """
{"events": [{"name":"event foo","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test "}, {"name":"event bar","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test2"}, {"name":"event foobar","date":"2018-07-21","time":"11:00","am_or_pm":"PM","day":"Saturday","description":"test3"}]}
"""
do {
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Root.self, from: data)
let grouped = Dictionary(grouping: result.events, by: { $0.date})
print(grouped)
} catch {
print("error: ", error)
}

Swift's JSONDecoder with multiple date formats in a JSON string?

Swift's JSONDecoder offers a dateDecodingStrategy property, which allows us to define how to interpret incoming date strings in accordance with a DateFormatter object.
However, I am currently working with an API that returns both date strings (yyyy-MM-dd) and datetime strings (yyyy-MM-dd HH:mm:ss), depending on the property. Is there a way to have the JSONDecoder handle this, since the provided DateFormatter object can only deal with a single dateFormat at a time?
One ham-handed solution is to rewrite the accompanying Decodable models to just accept strings as their properties and to provide public Date getter/setter variables, but that seems like a poor solution to me. Any thoughts?
Please try decoder configurated similarly to this:
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
}()
There are a few ways to deal with this:
You can create a DateFormatter subclass which first attempts the date-time string format, then if it fails, attempts the plain date format
You can give a .custom Date decoding strategy wherein you ask the Decoder for a singleValueContainer(), decode a string, and pass it through whatever formatters you want before passing the parsed date out
You can create a wrapper around the Date type which provides a custom init(from:) and encode(to:) which does this (but this isn't really any better than a .custom strategy)
You can use plain strings, as you suggest
You can provide a custom init(from:) on all types which use these dates and attempt different things in there
All in all, the first two methods are likely going to be the easiest and cleanest — you'll keep the default synthesized implementation of Codable everywhere without sacrificing type safety.
try this. (swift 4)
let formatter = DateFormatter()
var decoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
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
}
Swift 5
Actually based on #BrownsooHan version using a JSONDecoder extension
JSONDecoder+dateDecodingStrategyFormatters.swift
extension JSONDecoder {
/// Assign multiple DateFormatter to dateDecodingStrategy
///
/// Usage :
///
/// decoder.dateDecodingStrategyFormatters = [ DateFormatter.standard, DateFormatter.yearMonthDay ]
///
/// The decoder will now be able to decode two DateFormat, the 'standard' one and the 'yearMonthDay'
///
/// Throws a 'DecodingError.dataCorruptedError' if an unsupported date format is found while parsing the document
var dateDecodingStrategyFormatters: [DateFormatter]? {
#available(*, unavailable, message: "This variable is meant to be set only")
get { return nil }
set {
guard let formatters = newValue else { return }
self.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
for formatter in formatters {
if let date = formatter.date(from: dateString) {
return date
}
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
}
}
}
}
It is a bit of a hacky way to add a variable that can only be set, but you can easily transform var dateDecodingStrategyFormatters by func setDateDecodingStrategyFormatters(_ formatters: [DateFormatter]? )
Usage
lets say that you have already defined several DateFormatters in your code like so :
extension DateFormatter {
static let standardT: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return dateFormatter
}()
static let standard: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter
}()
static let yearMonthDay: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter
}()
}
you can now just assign these to the decoder straight away by setting dateDecodingStrategyFormatters :
// Data structure
struct Dates: Codable {
var date1: Date
var date2: Date
var date3: Date
}
// The Json to decode
let jsonData = """
{
"date1": "2019-05-30 15:18:00",
"date2": "2019-05-30T05:18:00",
"date3": "2019-04-17"
}
""".data(using: .utf8)!
// Assigning mutliple DateFormatters
let decoder = JSONDecoder()
decoder.dateDecodingStrategyFormatters = [ DateFormatter.standardT,
DateFormatter.standard,
DateFormatter.yearMonthDay ]
do {
let dates = try decoder.decode(Dates.self, from: jsonData)
print(dates)
} catch let err as DecodingError {
print(err.localizedDescription)
}
Sidenotes
Once again I am aware that setting the dateDecodingStrategyFormatters as a var is a bit hacky, and I dont recommend it, you should define a function instead. However it is a personal preference to do so.
Facing this same issue, I wrote the following extension:
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)")
}
})
}
}
This extension allows you to create a DateDecodingStrategy for the JSONDecoder that handles multiple different date formats within the same JSON String. The extension contains a function that requires the implementation of a closure that gives you a CodingKey, and it is up to you to provide the correct DateFormatter for the provided key.
Lets say that you have the following JSON:
{
"publication_date": "2017-11-02",
"opening_date": "2017-11-03",
"date_updated": "2017-11-08 17:45:14"
}
The following Struct:
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"
}
}
Then to decode the JSON, you would use the following code:
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)
It is a little verbose, but more flexible approach: wrap date with another Date class, and implement custom serialize methods for it. For example:
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")
}
}
}
So now you are independent of .dateDecodingStrategy and .dateEncodingStrategy and your MyCustomDate dates will parsed with specified format. Use it in class:
class User: Codable {
var dob: MyCustomDate
}
Instantiate with
user.dob = MyCustomDate(date)
There is no way to do this with a single encoder. Your best bet here is to customize the encode(to encoder:) and init(from decoder:) methods and provide your own translation for one these values, leaving the built-in date strategy for the other one.
It might be worthwhile looking into passing one or more formatters into the userInfo object for this purpose.
I have defined this extension on DateDecodingStrategy, that takes an array of date formatters as an argument:
extension JSONDecoder.DateDecodingStrategy {
static func anyFormatter(in formatters: [DateFormatter]) -> Self {
return .custom { decoder in
guard formatters.count > 0 else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No date formatter provided"))
}
guard let dateString = try? decoder.singleValueContainer().decode(String.self) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date string"))
}
let successfullyFormattedDates = formatters.lazy.compactMap { $0.date(from: dateString) }
guard let date = successfullyFormattedDates.first else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Date string \"\(dateString)\" does not match any of the expected formats (\(formatters.compactMap(\.dateFormat).joined(separator: " or ")))"))
}
return date
}
}
}
and I use it this way:
let format1 = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
let format2 = DateFormatter(format: "yyyy-MM-dd'T'HH:mmzzzzzz")
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .anyFormatter(in: [. format1, . format2])
It successively tries to convert the date using the different formatters provided (lazily, so that it stops after the first successful attempt), and if every format fails then it throws an error.
If you have multiple dates with different formats in single model, its bit difficult to apply .dateDecodingStrategy for each dates.
Check here https://gist.github.com/romanroibu/089ec641757604bf78a390654c437cb0 for a handy solution
Add an extension to KeyedDecodingContainer
extension KeyedDecodingContainer {
func decodeDate(forKey key: KeyedDecodingContainer<K>.Key, withPossible formats: [DateFormatter]) throws -> Date? {
for format in formats {
if let date = format.date(from: try self.decode(String.self, forKey: key)) {
return date
}
}
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Date string does not match format expected by formatter.")
}
}
and use 'try container.decodeDate(forKey: 'key', withPossible: [.iso8601Full, .yyyyMMdd])'
full solution is here:
import Foundation
extension DateFormatter {
static let iso8601Full: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}
public struct RSSFeed: Codable {
public let releaseDate: Date?
public let releaseDateAndTime: Date?
}
extension RSSFeed {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
releaseDate = try container.decodeDate(forKey: .releaseDate, withPossible: [.iso8601Full, .yyyyMMdd])
releaseDateAndTime = try container.decodeDate(forKey: .releaseDateAndTime, withPossible: [.iso8601Full, .yyyyMMdd])
}
}
extension KeyedDecodingContainer {
func decodeDate(forKey key: KeyedDecodingContainer<K>.Key, withPossible formats: [DateFormatter]) throws -> Date? {
for format in formats {
if let date = format.date(from: try self.decode(String.self, forKey: key)) {
return date
}
}
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Date string does not match format expected by formatter.")
}
}
let json = """
{
"releaseDate":"2017-11-12",
"releaseDateAndTime":"2017-11-16 02:02:55"
}
"""
let data = Data(json.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
let rssFeed = try! decoder.decode(RSSFeed.self, from: data)
let feed = rssFeed
print(feed.releaseDate, feed.releaseDateAndTime)