How to handle a mix of standard and customized Swift 4 Decodable properties? - swift4

I've been experimenting with customized Decodable properties for handling JSON in Swift 4 and I'm really impressed with the ease of mapping tricky type and format conversions.
However in the JSON data structures that the server exposes to me, only a handful of properties need this treatment. The rest are simple integers and strings. Is there some way to mix the customized decoder with the standard one?
Here's a simplified example showing what I'd like to get rid of:
struct mystruct : Decodable {
var myBool: Bool
var myDecimal: Decimal
var myDate: Date
var myString: String
var myInt: Int
}
extension mystruct {
private struct JSONsource: Decodable {
var my_Bool: Int
var my_Decimal: String
var my_Date: String
// These seem redundant, how can I remove them?
var myString: String
var myInt: Int
}
private enum CodingKeys: String, CodingKey {
case item
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let item = try container.decode(JSONsource.self, forKey: .item)
myBool = item.my_Bool == 1 ? true : false
myDecimal = Decimal(string: item.my_Decimal)!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSZZZZZ"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
myDate = dateFormatter.date(from: item.my_Date)!
// Can I somehow get rid of this redundant-looking code?
myString = item.myString
myInt = item.myInt
}
}
let myJSON = """
{
"item": {
"my_Decimal": "123.456",
"my_Bool" : 1,
"my_Date" : "2019-02-08T11:14:31.4547774-05:00",
"myInt" : 148727,
"myString" : "Hello there!"
}
}
""".data(using: .utf8)
let x = try JSONDecoder().decode(mystruct.self, from: myJSON!)
print("My decimal: \(x.myDecimal)")
print("My bool: \(x.myBool)")
print("My date: \(x.myDate)")
print("My int: \(x.myInt)")
print("My string: \(x.myString)")

JSONDecoder has a dateDecodingStrategy. No need to decode it manually. To simplify decoding your coding keys you can set decoders property .keyDecodingStrategy to . convertFromSnakeCase. You have also some type mismatches you can handle adding computed properties. Btw this might help creating a custom formatter for your ISO8601 date string with fractional seconds. How to create a date time stamp and format as ISO 8601, RFC 3339, UTC time zone? . Last but not least, it is Swift convention to name your structures using UpperCamelCase
struct Root: Codable {
let item: Item
}
struct Item : Codable {
var myBool: Int
var myDecimal: String
var myDate: Date
var myString: String
var myInt: Int
}
extension Item {
var bool: Bool {
return myBool == 1
}
var decimal: Decimal? {
return Decimal(string: myDecimal)
}
}
extension Item: CustomStringConvertible {
var description: String {
return "Iten(bool: \(bool), decimal: \(decimal ?? 0), date: \(myDate), string: \(myString), int: \(myInt))"
}
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSZZZZZ"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
decoder.dateDecodingStrategy = .formatted(dateFormatter)
do {
let item = try decoder.decode(Root.self, from: myJSON).item
print(item) // Iten(bool: true, decimal: 123.456, date: 2019-02-08 16:14:31 +0000, string: Hello there!, int: 148727)
}
catch {
print(error)
}

Related

Filter an Array of Structs by Months? SwiftUI

Hey guys I got something very similar going on in my project. But I was wondering when you have an array of structs, how would you be able to filter for items in specific month? Also the date is in epoch form.
import SwiftUI
struct TestStruct {
var food: String
var date: Double
}
class Test: ObservableObject{
let food1 = TestStruct(food: "Hamburger", date: 1641058794)
let food2 = TestStruct(food: "HotDog", date: 1651426794)
let food3 = TestStruct(food: "icecream", date: 1652204394)
let foodForYear: [TestStruct] = [food1, food2, food3]
}
You can achieve that by using a simple filter on the array, and getting the TimeInterval you need in order to do the filter, here is a simple example:
import Foundation
struct TestStruct {
var food: String
var date: Double
var parsedDate: Date {
Date(timeIntervalSince1970: date)
}
var monthNumber: Int {
Calendar.current.component(.month, from: parsedDate)
}
}
class Test: ObservableObject{
let food1 = TestStruct(food: "Hamburger", date: 1641058794)
let food2 = TestStruct(food: "HotDog", date: 1651426794)
let food3 = TestStruct(food: "icecream", date: 1652204394)
let foodForYear: [TestStruct] = [food1, food2, food3]
func filterMonthy() {
let fiteredByMonths = foodForYear.filter({$0.monthNumber == 1}) // any logic you want to apply
print(fiteredByMonths)
}
}
Take a look at the documentation: Date
The issue is that you are using epoch form itself instead of native Date:
struct TestStruct {
var food: String
var date: Date
init(food: String, date: Double) {
self.food = food
self.date = Date(timeIntervalSince1970: date)
}
}
In this case you can easy get month or year in a string format:
extension TestStruct {
var monthName: String {
return date.formatted(
Date.FormatStyle()
.year(.defaultDigits) // remove if you don't need it
.month(.abbreviated) // you can choose .wide if you want other format or read help for more information
)
}
}
Now let's check it:
let foodForYear: [TestStruct] = ...
print(foodForYear.sorted{ $0.monthName > $1.monthName}) // sorting
let nameOfMonth = "SomeMonthName" // depends from your format style
print(foodForYear
.compactMap{
if $0.monthName.contains(nameOfMonth) {
return $0
} else { return nil }
}
)

Swift codable - how to get property value of struct located higher up in the hierarchy?

Attempting to refactor some legacy JSON parsing code to use Codable and attempting to reuse the existing Swift structs, for simplicity pls consider the following JSON:
{
"dateOfBirth":"2016-05-19"
...
"discountOffer":[
{
"discountName":"Discount1"
...
},
{
"discountName":"Discount2"
...
}
]
}
In the legacy code, the Swift struct Discount has a property 'discountType' whose value is computed based on Member struct's 'dateOfBirth' which is obtained from the JSON, question is, how do I pass the Member's dateOfBirth down to the each Discount struct? Or is there a way for structs lower in the hierarchy to access structs higherup in the hierarchy?
struct Member: Codable {
var dateOfBirth: Date?
var discounts: [Discount]?
}
struct Discount: Codable {
var discountName: String?
var memberDateOfBirth: Date? // *** Need to get it from Member but how?
var discountType: String? // *** Will be determined by Member's dateOfBirth
public init(from decoder: Decoder) throws {
// self.memberDateOfBirth = // *** How to set from Member's dateOfBirth?????
// use self.memberDateOfBirth to determine discountType
...
}
}
I am not able to use the decoder's userInfo as its a get property. I thought of setting the dateOfBirth as a static variable somewhere but sounds like a kludge.
Would appreciate any help. Thanks.
You should handle this in Member, not Discount, because every Codable type must be able to be decoded independently.
First, add this to Discount so that only the name is decoded:
enum CodingKeys : CodingKey {
case discountName
}
Then implement custom decoding in Member:
enum CodingKeys: CodingKey {
case dateOfBirth, discounts
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dateOfBirth = try container.decode(Date.self, forKey: .dateOfBirth)
discounts = try container.decode([Discount].self, forKey: .discounts)
for i in 0..<discounts!.count {
discounts![i].memberDateOfBirth = dateOfBirth
}
}
The for loop at the end is where we give values to the discounts.
Going back to Discount, you can either make discountType a computed property that depends on memberDateOfBirth, or add a didSet observer to memberDateOfBirth, where you set discountType.
var discountType: String? {
if let dob = memberDateOfBirth {
if dob < Date(timeIntervalSince1970: 0) {
return "Type 1"
}
}
return "Type 2"
}
// or
var memberDateOfBirth: Date? {
didSet {
if let dob = memberDateOfBirth {
if dob < Date(timeIntervalSince1970: 0) {
discountType = "Type 1"
}
}
discountType = "Type 2"
}
}
You can access them like this
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.memberDateOfBirth = try values.decode(T.self, forKey: .result) //and whatever you want to do
serverErrors = try values.decode([ServerError]?.self, forKey: .serverErrors)
}
you can try in this way:
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateString = try container.decode(String.self, forKey: .memberDateOfBirth)
let formatter = "Your Date Formatter"
if let date = formatter.date(from: dateString) {
memberDateOfBirth = date
}
if you want to know more check this approach:
https://useyourloaf.com/blog/swift-codable-with-custom-dates/
For Dateformatter you can check :
Date Format in Swift
https://nsdateformatter.com

UTF-8 encoding issue of JSONSerialization

I was trying convert struct to Dictionary in Swift. This was my code:
extension Encodable {
var dictionary: [String: Any]? {
if let data = try? JSONEncoder().encode(self) {
if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
return dict
}
return nil
}
return nil
}
}
This works in most situation. But when I try to convert a nested structure which contains unicode characters such as Chinese, this happened:
struct PersonModel: Codable {
var job: String?
var contacts: [ContactSimpleModel]
var manager: ManagerSimpleModel?
}
struct ContactSimpleModel: Codable {
var relation: String
var name: String
}
struct ManagerSimpleModel: Codable {
var name: String
var age: Int
}
let contact1 = ContactSimpleModel(relation: "朋友", name: "宙斯")
let contact2 = ContactSimpleModel(relation: "同学", name: "奥丁")
let manager = ManagerSimpleModel(name: "拉斐尔", age: 31)
let job = "火枪手"
let person = PersonModel(job: job, contacts: [contact1, contact2], manager: manager)
if let dict = person.dictionary {
print(dict)
}
The result of this code is this:
["contacts": <__NSArrayI 0x600002471980>(
{
name = "\U5b99\U65af";
relation = "\U670b\U53cb";
},
{
name = "\U5965\U4e01";
relation = "\U540c\U5b66";
}
)
, "manager": {
age = 31;
name = "\U62c9\U6590\U5c14";
}, "job": 火枪手]
You can see the result. The Chinese characters in those nested structures were become a utf-8 encoding string. The top-level property "job": 火枪手 is right. But the values in those nested structures were not the original string.
Is this a bug of JSONSerialization? Or how to make it right?
More information. I used the result like this:
var sortedQuery = ""
if let dict = person.dictionary {
sortedQuery = dict.sorted(by: {$0.0 < $1.0})
.map({ "\($0)\($1)" })
.joined(separator: "")
}
It was used to check whether the query was legal. The result is not the same as Java or other platform.
The result is perfectly fine. That's the internal string representation – a pre-Unicode legacy – of an array or dictionary when you print it.
Assign the values to a label or text view and you will see the expected characters.

How to parse json with Decodable ?

when i try to parse my json with decodable birthday comes nil.
What date format should I use any advice or code sample please.
my date format include timezone.
My problem is birthdate comes nil. How to parse birthdate with decodable ?
My json :
{
"id": 1,
"name": "fatih",
"birddate": "2018-09-19T11:36:00.4033163+03:00",
"total": 0.9,
"isTest": false
}
here is my struct :
struct TestDTO : Decodable {
var id:Int?
var name : String?
var birtdate : Date?
var total : Double?
var isTest : Bool?
}
RestClientServiceTest().CallRestService(matching: cmd, completion: { (data) in
do{
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let stories = try decoder.decode(TestDTO.self, from: data!)
print(data)
}catch let error{
print("Json Parse Error : \(error)")
}
})
So, having a bit of play in playground...
let format = DateFormatter()
format.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
let value = "2018-09-19T11:36:00.4033163+03:00"
print(format.date(from: value))
Prints 2018-09-19 08:36:00 +0000
So taking that a leap further...
let format = DateFormatter()
format.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
let text = """
{
"id": 1,
"name": "fatih",
"birddate": "2018-09-19T11:36:00.4033163+03:00",
"total": 0.9,
"isTest": false
}
"""
struct TestDTO : Decodable {
var id:Int?
var name : String?
var birddate : Date?
var total : Double?
var isTest : Bool?
}
do{
let jsonData = text.data(using: .utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(format)
let stories = try decoder.decode(TestDTO.self, from: jsonData!)
print(stories)
}catch let error{
print("Json Parse Error : \(error)")
}
prints...
TestDTO(id: Optional(1), name: Optional("fatih"), birddate: Optional(2018-09-19 08:36:00 +0000), total: Optional(0.9), isTest: Optional(false))
You might find Easy Skeezy Date Formatting for Swift of some use
First of all the name you used in struct is wrong birddate - typo error.
Second don't set it as Date it's an String.
Get the data as String and while using it go with NSDateFormatter.
You can search for NSDateFormatter on SO.
Date Format in Swift

Swift Date literal / Date initialisation from string

I need to initialize a date to a known, fixed, point in time. Swift lacking date literals, I tried :
extension DateFormatter
{
convenience init(_ format: String)
{
self.init()
self.dateFormat = format
}
}
extension Date
{
init?(_ yyyyMMdd: String)
{
let formatter = DateFormatter("yyyyMMdd")
self = formatter.date(from: yyyyMMdd)
}
}
Unfortunately, I can't write self = ... or return formatter.date... as part of the initializer.
However, I would very much like to write :
let date = Date("20120721")
How can I achieve this ?
You can initialize a struct by assigning a value to self.
The problem in your case is that formatter.date(from:)
returns an optional, so you have to unwrap that first:
extension Date {
init?(yyMMdd: String) {
let formatter = DateFormatter("yyyyMMdd")
guard let date = formatter.date(from: yyMMdd) else {
return nil
}
self = date
}
}
You can even initialize a date from a string literal by
adopting the ExpressibleByStringLiteral protocol:
extension Date: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
let formatter = DateFormatter("yyyyMMdd")
self = formatter.date(from: value)!
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(stringLiteral: value)
}
public init(unicodeScalarLiteral value: String) {
self.init(stringLiteral: value)
}
}
let date: Date = "20120721"
Note however that this would crash at runtime for invalid dates.
Two more remarks:
You should set the formatters locale to Locale(identifier: "en_US_POSIX") to allow parsing the date string independent of
the user's locale settings.
The result depends on the current time zone, unless you also
set the formatters time zone to a fixed value (such as TimeZone(secondsFromGMT: 0).