Trying to understand complex enum in Swift 5 - swift

I am using a package that has the following in it:
public enum DateValue {
case dateOnly(Date)
case dateAndTime(Date)
}
extension DateValue: Codable {
static let errorMessage = "Date string does not match format expected by formatter."
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(String.self)
if let date = DateFormatter.iso8601Full.date(from: value) {
self = .dateAndTime(date)
return
}
if let date = DateFormatter.iso8601DateOnly.date(from: value) {
self = .dateOnly(date)
return
}
throw Swift.DecodingError.dataCorruptedError(
in: container,
debugDescription: Self.errorMessage
)
}
public func encode(to encoder: Encoder) throws {
let value: String
switch self {
case .dateOnly(let date):
value = DateFormatter.iso8601DateOnly.string(from: date)
case .dateAndTime(let date):
value = DateFormatter.iso8601Full.string(from: date)
}
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
In Xcode 13.2.1 I see this in the debugger:
So, I have theStartDate as a DataValue type but want to access it as a Foundation.Date. I would think this is simple, as I am sure it is, but this is over my head in Swift skills.
I am truly at a loss for how to do this and I think my main issue is not understanding how the emun and extension work at all. I have searched around but another problem I have is even defining what I am searching for since I am not sure what I am looking at to begin with. Just trying "theStartDate as Date" doesn't work.

I see I was getting confused by the extra code now and did not realize this is the same problem I have already solved. This code is working now:
func getDateFromKey(thePage: Page, theKey: String) -> Date? {
if let theProperty = thePage.properties[theKey] {
if case .date(let theDateValue) = theProperty.type {
if case .dateOnly(let theDate) = theDateValue!.start {
return theDate
}
}
}
return nil
}

Related

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

Contentful error when using client.fetchArray

I'm getting the following error when trying to follow this tutorial:
Oh no something went wrong: A response for the QueryOn<Thing> did return successfully, but a serious error occurred when decoding the array of Thing.
Double check that you are passing Thing.self, and references to all other EntryDecodable classes into the Client initializer.
when using the following code to call contentful:
func fetch() {
let query = QueryOn<Thing>.where(field: .description, .exists(true))
client.fetchArray(of: Thing.self, matching: query) { (result: Result<ArrayResponse<Thing>>) in
switch result {
case .success(let things):
guard let firstThing = things.items.first else { return }
print(firstThing)
case .error(let error):
print("Oh no something went wrong: \(error)")
}
}
}
My Thing model is set as so:
and i currently have two Things added:
My Thing class looks like so:
final class Thing: EntryDecodable, FieldKeysQueryable {
enum FieldKeys: String, CodingKey {
case name, description
}
static let contentTypeId: String = "thing"
let id: String
let localeCode: String?
let updatedAt: Date?
let createdAt: Date?
let name: String
let description: String
public required init(from decoder: Decoder) throws {
let sys = try decoder.sys()
id = sys.id
localeCode = sys.locale
updatedAt = sys.updatedAt
createdAt = sys.createdAt
let fields = try decoder.contentfulFieldsContainer(keyedBy: Thing.FieldKeys.self)
self.name = try! fields.decodeIfPresent(String.self, forKey: .name)!
self.description = try! fields.decodeIfPresent(String.self, forKey: .description)!
}
}
Can anyone see what im missing?
so Contentful's documentation is all over the place. I had the same issue, but I managed to solve it after checking their documentation in their GitHub repo itself.
Basically you need to pass all Swift classes that conform the 'EntryDecodable' and 'FieldKeysQueryable' inside the Client initializer method.
Hope it helps!
Just wanted to make it easier for people visiting. All you need to do is ensure you've passed in your contentTypeClasses into the client Init method.
https://github.com/contentful/contentful.swift#map-contentful-entries-to-swift-classes-via-entrydecodable
fileprivate let client = Client(spaceId: spaceKey, accessToken: contentDeliveryKey, contentTypeClasses: [YourCustomClass.self])
final class YourCustomClass: EntryDecodable, FieldKeysQueryable {
static let contentTypeId: ContentTypeId = "yourCustomContentfulType"
public required init(from decoder: Decoder) throws {
let sys = try decoder.sys()
id = sys.id
localeCode = sys.locale
updatedAt = sys.updatedAt
createdAt = sys.createdAt
}
Thanks #Wazza for sharing the link to contentful's github docs.

Decode string or int value in structure will return string("1") in Swift 4

I have a structure that some of the values will get Integer or string so I used this structure in my structure to parse JSON in the correct way but the problem is that when I want to print the value it will print string("1") instead of 1.
public struct player1_result_sheet : Decodable {
let ans_1 : QuantumValue?
let ans_2 : QuantumValue?
let ans_3 : QuantumValue?
let ans_4 : QuantumValue?
}
enum QuantumValue: Decodable {
case int(Int), string(String)
init(from decoder: Decoder) throws {
if let int = try? decoder.singleValueContainer().decode(Int.self) {
self = .int(int)
return
}
if let string = try? decoder.singleValueContainer().decode(String.self) {
self = .string(string)
return
}
throw QuantumError.missingValue
}
enum QuantumError:Error {
case missingValue
}
}
Here is the printing after decode:
(self.res?.response?.detailData?[indexPath.row].player1_result_sheet?.ans_1!)!
If you want to print the value without the enum wrapper, just implement description:
extension QuantumValue: CustomStringConvertible {
public var description: String {
switch self {
case let .string(string):
return string
case let .int(number):
return "\(number)"
}
}
}
Your enum case is string(String) so it prints case(value)
string("1")
You can solve it by creating varible inside enum which returns you a value depends on case of QuantumValue
var value: Any {
switch self {
case .int(let value):
return value
case .string(let value):
return value
}
}
then you can use it like this:
...ans_1!.value)!
1
Also note that type of value is Any so if you want to work with it as String, you have to downcast it
if let string = ...ans_1.value)! as? String {
...
}
QuantumValue is declared as enum, both good cases have associated values.
So printing a value prints both, the case and the associated value.
You could add two properties intValue and stringValue inside QuantumValue
var intValue : Int? {
guard case .int(let num) = self else { return nil }
return num
}
var stringValue : String? {
guard case .string(let string) = self else { return nil }
return string
}
Then you can print
player1_result_sheet?.ans_1?.intValue
By the way the name player1_result_sheet is pretty php-ish.
Please conform to the naming convention.
Structs, classes and enums are UpperCamelCased → Player1ResultSheet
Variables and functions are lowerCamelCased → ans1
And please consolidate your optionals, 6 (six) question and exclamation marks in one line is pretty weird
(self.res?.response?.detailData?[indexPath.row].player1_result_sheet?.ans_1!)!

JSONEncoder won't allow type encoded to primitive value

I'm working on an implementation of Codable for an enum type with possible associated values. Since these are unique to each case, I thought I could get away with outputting them without keys during encoding, and then simply see what I can get back when decoding in order to restore the correct case.
Here's a very much trimmed down, contrived example demonstrating a sort of dynamically typed value:
enum MyValueError : Error { case invalidEncoding }
enum MyValue {
case bool(Bool)
case float(Float)
case integer(Int)
case string(String)
}
extension MyValue : Codable {
init(from theDecoder:Decoder) throws {
let theEncodedValue = try theDecoder.singleValueContainer()
if let theValue = try? theEncodedValue.decode(Bool.self) {
self = .bool(theValue)
} else if let theValue = try? theEncodedValue.decode(Float.self) {
self = .float(theValue)
} else if let theValue = try? theEncodedValue.decode(Int.self) {
self = .integer(theValue)
} else if let theValue = try? theEncodedValue.decode(String.self) {
self = .string(theValue)
} else { throw MyValueError.invalidEncoding }
}
func encode(to theEncoder:Encoder) throws {
var theEncodedValue = theEncoder.singleValueContainer()
switch self {
case .bool(let theValue):
try theEncodedValue.encode(theValue)
case .float(let theValue):
try theEncodedValue.encode(theValue)
case .integer(let theValue):
try theEncodedValue.encode(theValue)
case .string(let theValue):
try theEncodedValue.encode(theValue)
}
}
}
let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)
However, this is giving me an error during the encoding stage as follows:
"Top-level MyValue encoded as number JSON fragment."
The issue appears to be that, for whatever reason, the JSONEncoder won't allow a top-level type that isn't a recognised primitive to be encoded as a single primitive value. If I change the singleValueContainer() to an unkeyedContainer() then it works just fine, except that of course the resulting JSON is an array, not a single value, or I can use a keyed container but this produces an object with the added overhead of a key.
Is what I'm trying to do here impossible with a single value container? If not, is there some workaround that I can use instead?
My aim was to make my type Codable with a minimum of overhead, and not just as JSON (the solution should support any valid Encoder/Decoder).
There is a bug report for this:
https://bugs.swift.org/browse/SR-6163
SR-6163: JSONDecoder cannot decode RFC 7159 JSON
Basically, since RFC-7159, a value like 123 is valid JSON, but JSONDecoder won't support it. You may follow up on the bug report to see any future fixes on this. [The bug was fixed starting in iOS 13.]
#Where it fails#
It fails in the following line of code, where you can see that if the object is not an array nor dictionary, it will fail:
https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/JSONSerialization.swift#L120
open class JSONSerialization : NSObject {
//...
// top level object must be an Swift.Array or Swift.Dictionary
guard obj is [Any?] || obj is [String: Any?] else {
return false
}
//...
}
#Workaround#
You may use JSONSerialization, with the option: .allowFragments:
let jsonText = "123"
let data = Data(jsonText.utf8)
do {
let myString = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(myString)
}
catch {
print(error)
}
Encoding to key-value pairs
Finally, you could also have your JSON objects look like this:
{ "integer": 123456 }
or
{ "string": "potatoe" }
For this, you would need to do something like this:
import Foundation
enum MyValue {
case integer(Int)
case string(String)
}
extension MyValue: Codable {
enum CodingError: Error {
case decoding(String)
}
enum CodableKeys: String, CodingKey {
case integer
case string
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodableKeys.self)
if let integer = try? values.decode(Int.self, forKey: .integer) {
self = .integer(integer)
return
}
if let string = try? values.decode(String.self, forKey: .string) {
self = .string(string)
return
}
throw CodingError.decoding("Decoding Failed")
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodableKeys.self)
switch self {
case let .integer(i):
try container.encode(i, forKey: .integer)
case let .string(s):
try container.encode(s, forKey: .string)
}
}
}
let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
print(theEncodedString!) // { "integer": 123456 }
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)

Swift Codable: dealing with Optimized JSON generating type Mismatch

I am trying to figure out the correct or best way to deal with APIs that sends out Optimized JSON.
If you have not encontered Optimized JSON as a server respons here is a bit of background, say you have a response for a #color code as #FF2233 the server sends out {"color" : "FF2233"},
But if the code is 223344 then the server sends out {"color" : 223344} cutting out quotes
Same for a single object array it send the object with out the brackets
So far this is what I have in a playground as a test... it works but it seems to me this is not the best way to get around the problem I am trying to solve.
Is there any way to check the type of a value before attempting to decode it so we can at least try to cast it correctly ??
struct Test : Codable {
var a : String?
enum CodingKeys: String, CodingKey {
case a = "a"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do{
a = try values.decodeIfPresent(String.self, forKey: .a)
}
catch{
a = String(describing: try values.decodeIfPresent(Int.self, forKey: .a)!)
}
}
init(a: String)
{
self.a = a
}
}
var a = Test(a: "FF2233")
var jd = try? JSONEncoder().encode(a)
var jt = String(bytes: jd!, encoding: .utf8)
jt = "{\"a\":223365}"
jd = jt?.data(using: .utf8)
do{
let res = try JSONDecoder().decode(Test.self, from: jd!)
print(res.a!)
} catch{
print(error)
}
What you're dealing with here is, strictly, broken JSON. Your approach is necessary. It's stupid, but it's only necessary because someone made a stupid decision to send you broken JSON.
You could clean up the code a little by extending KeyedDecodingContainer to wrap up these changes in a method called something like lenientDecode(). Then you could write something like a = values.lenientDecode(String.self, forKey:.a). You'd still do the same checks, but putting them in a separate method would make it easier to repeat the checks on multiple fields.
I found it so interesting when trying to deal with your data type.
Let's break it down to the underlying type first. At the lowest level, you have Int or String. Then you have Single object or Array. And at the highest level you should have a struct which can deal with your Root object. Basically you need two enums to wrap your struct. Let's try:
enum IntOrString: Codable {
case int(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .int(container.decode(Int.self))
} catch DecodingError.typeMismatch {
do {
self = try .string(container.decode(String.self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(IntOrString.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type, (Int or String)"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let int):
try container.encode(int)
case .string(let string):
try container.encode(string)
}
}
}
enum SingleOrArray: Codable {
case single(IntOrString)
case array([IntOrString])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .single(container.decode(IntOrString.self))
} catch DecodingError.typeMismatch {
do {
self = try .array(container.decode([IntOrString].self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(SingleOrArray.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .single(let single):
try container.encode(single)
case .array(let array):
try container.encode(array)
}
}
}
struct Root: Codable {
let color: SingleOrArray
}
Decoding Process:
let jsonData = """
{
"color":["FF2233", "FF2234", "FF2235"]
}
""".data(using: .utf8)!
do {
let response = try JSONDecoder().decode(Root.self, from: jsonData)
//print(response) //["FF2233", "FF2234", "FF2235"]
// If you want to get underlying elements from this structure, you might do something like below
// This is your single object
if case .single(let single) = response.color {
// Every single object may be an Int or a String
if case .int(let int) = single {
print(int)
}
if case .string(let string) = single {
print(string)
}
}
// This is your array
if case .array(let array) = response.color {
array.forEach({ (element) in
// Each element of your array may be an Int or a String
if case .int(let int) = element {
print(int)
}
if case .string(let string) = element {
print(string)
}
})
}
} catch {
print(error)
}
The data types that this can handle are:
You can try replacing the value for key color in the above JSON with one of these objects
String: "FF2233"
Int: 223344
[String]: ["FF2233", "FF2234", "FF2235"]
[Int]: [223344, 223345, 223346]
Array of String/Int mixed: ["FF2233", "FF2234", 223344, 223345, "FF2235", 223346]
The most surprising fact about this design is that you can parse Mixture of [Int & String]