Assign dynamically properties of a Struct in Swift - swift

I have this Struct:
struct Alphabet {
let a = "ciao"
let b = "hi"
let c = "hola"
}
let alphabet = Alphabet()
I want the value of each property become the string of the property itself.
Like this:
alphabet.a = "a"
alphabet.b = "b"
alphabet.c = "c"
But I want to be done regardless of the number of properties or their value:
I tried this:
Mirror(reflecting: Alphabet.self).children.forEach { (label, value) in
self.alphabet[keyPath: label] = label!
}
but I know this is not the way KeyPath works...
Probably there are type safety problems going on too.
Any idea?

As far as I know keyPaths aren't the way to go, you need to use CodingKeys
Here's a working sample, creating JSON and then decoding it might not be the perfect so you better change my solution to suit your needs.
struct Alphabet: Codable {
let a: String
let b: String
let c: String
enum CodingKeys: String, CodingKey, CaseIterable
{
case a
case b
case c
}
static func generateJSON() -> String {
var json = "{"
for i in CodingKeys.allCases
{
json += "\"\(i.stringValue)\": \"\(i.stringValue)\","
}
json.removeLast()
json += "}"
return json
}
}
let decoder = JSONDecoder()
let alphabet = try decoder.decode(Alphabet.self, from: Alphabet.generateJSON().data(using: .utf8)!)
print(alphabet.a) //Prints "a"

Related

Swift 5 JSON Import with CodingKeys and ENUM values

Reading in from small JSON file (cut/pasted small sample) and bring it into structs. I am using CodingKeys to change the "string key" to match what I want in the struct. I am changing the "whoresponds" key to "respondingPilot". That is all working fine. However, I also want to look at the values on that property and change on-the-fly the value brought in. For example, if I get the string "BOTH", I want to change my data that is stored to "CAPT & F/O". Same if I see "FO", I want that changed to "F/O" as it read in. Sample below reads in fine but will not make the changes. Still learning but must be something simple I am missing. Thanks!
struct CheckListsJSON: Codable {
var name: String
var checklistItems: [ChecklistItemsJSON]
}
struct ChecklistItemsJSON: Codable, Identifiable {
var challenge: String
var respondingPilot: PilotResponding
let id = UUID()
private enum CodingKeys: String, CodingKey {
case challenge
case respondingPilot = "whoresponds"
}
enum PilotResponding: String, Codable {
case CPT = "CAPT"
case FO = "F/O"
case PF = "PF"
case PM = "PM"
case BOTH = "CAPT & F/O"
}
}
let jsonAC = "{\"name\": \"After Start\", \"checklistItems\": [{\"challenge\": \"Generators\", \"whoresponds\": \"CPT\"}, {\"challenge\": \"Isolation Valve\", \"whoresponds\": \"FO\"}}"
let fileData = Data(jsonAC.utf8)
do {
let decodedData = try JSONDecoder().decode(CheckListsJSON.self, from: fileData)
print("decoded:", decodedData)
} catch {
print(error)
}
Decoding a string to an enum works like CodingKeys. The raw value is the string you receive and the case is the case.
Unfortunately the character set of enum cases is restricted. Space characters, / and & are not allowed.
You could write something like this, raw values which match the case can be omitted.
enum PilotResponding: String, Codable {
case CAPT = "CPT"
case F_O = "FO"
case PF, PM
case CAPT_F_O = "BOTH"
}

Cannot iterate through array inside array of dictionaries

I'm having trouble parsing a response into workable objects in swift.
Basically, this is the response I get (simplified data for viewing purposes)
"[{\"CO2\":0,\"Places\":[{\"Name\":\"RT(Esc. Sec.)\",\"Code\":\"ST_RT\",\"Type\":0,\"CoordX\":41.176750183105469,\"CoordY\":-8.5490522384643555,\"Provider\":\"ST\",\"Lines\":null},{\"Name\":\"Esc.Sec RT\",\"Code\":\"ST_RT2\",\"Type\":0,\"CoordX\":41.175251007080078,\"CoordY\":-8.54929256439209,\"Provider\":\"ST\",\"Lines\":null},{\"Name\":\"SM\",\"Code\":\"ST_SM\",\"Type\":0,\"CoordX\":41.173740386962891,\"CoordY\":-8.5474367141723633,\"Provider\":\"ST\",\"Lines\":null}],\"Direction\":\"R\"}]"
After I receive the response I do the following:
let dict = try! JSONSerialization.jsonObject(data: responseData!, options: .allowFragments) as? [[String:Any]] ?? [[String:Any]]()
Which results in the following dictionary (I'm sorry for the picture, but I could not take a print screen. Plus those 44 elements are the total number of Places, but I've simplified the response string as I said above)
My problem is, I cannot access each Place dictionary. I've tried iterating through dict["Places"] but that does not seem to work, which I do not understand, given it is an NSArray.
I may be missing something simple, but I can't seem to figure it out.
Any help is welcome.
Thanks!
You can try
// MARK: - Root
struct Root: Codable {
let co2: Int
let places: [Place]
let direction: String
enum CodingKeys: String, CodingKey {
case co2 = "CO2"
case places = "Places"
case direction = "Direction"
}
}
// MARK: - Place
struct Place: Codable {
let name, code: String
let type: Int
let coordX, coordY: Double
let provider: String
let lines: String?
enum CodingKeys: String, CodingKey {
case name = "Name"
case code = "Code"
case type = "Type"
case coordX = "CoordX"
case coordY = "CoordY"
case provider = "Provider"
case lines = "Lines"
}
}
let res = try JSONDecoder().decode([Root].self,from:data)
print(res.places)

Swift Codable:Nested Dictionary not being read

I am getting some json like so (this is pseudo, not all keys are here):
{
"absolute_magnitude_h" = "23.4";
...
"close_approach_data" = (
{
"close_approach_date" = "1994-09-03";
"epoch_date_close_approach" = 778575600000;
"orbiting_body" = Earth;
}
I have an object with this struct:
struct NEOObj:Codable {
var absoluteMagnitudeH:Float
var designation:String
var isPotentiallyHazardousAsteroid:Bool
var isSentryObject:Bool
var name:String
var nasaJPLURL:String
var neoReferenceID:String
var closeApproachData:[NEOCloseApproachData] = [NEOCloseApproachData]()
private enum CodingKeys: String, CodingKey {
case absoluteMagnitudeH = "absolute_magnitude_h"
case designation = "designation"
case isPotentiallyHazardousAsteroid = "is_potentially_hazardous_asteroid"
case isSentryObject = "is_sentry_object"
case name = "name"
case nasaJPLURL = "nasa_jpl_url"
case neoReferenceID = "neo_reference_id"
}
enum CloseApproachCodingKeys: String, CodingKey {
case closeApproachDate = "close_approach_date"
case epochDateCloseApproach = "epoch_date_close_approach"
case orbitingBody = "orbiting_body"
}
struct NEOCloseApproachData:Codable {
var closeApproachDate:Date
var epochDateCloseApproach:Date
var orbitingBody:String
enum CodingKeys: String, CodingKey {
case closeApproachDate = "close_approach_date"
case epochDateCloseApproach = "epoch_date_close_approach"
case orbitingBody = "orbiting_body"
}
}
and in my file I have this code:
if let arrNEOs = dictJSON["near_earth_objects"] as? Array<Any> {
for thisNEODict in arrNEOs {
do {
let jsonData = try JSONSerialization.data(withJSONObject: thisNEODict, options: .prettyPrinted)
let thisNEOObj = try? JSONDecoder().decode(NEOObj.self, from: jsonData)
print(thisNEOObj!.closeApproachData)
} catch {
}
}
}
But closeApproachData never gets populated. What am I doing wrong?
There are multiple things that would need to be changed for it to work.
closeApproachData should probably be of type [NEOCloseApproachData]. You haven't included CloseApproachCodingKeys in your code but it probably isn't right.
NEOObj.CodingKeys needs to have a case for closeApproachData which would look like
case closeApproachData = "close_approach_data"
NEOCloseApproachData needs a custom decoder to work with the dates for closeApproachDate and epochDateCloseApproach. Since the JSON has different formats for each (String and Int respectively), you can't use on the JSONDecoder.dateDecodingStrategy since it will apply to all dates.
The reason why after decoding you get closeApproachData property empty is the lack of the regarding key for that property into your CodingKeys enum.
Regarding to the Apple doc, it tells next:
Omit properties from the CodingKeys enumeration if they won't be
present when decoding instances, or if certain properties shouldn't be
included in an encoded representation.
That means that generated implementation of decode method for your NEOObj class omit decoding implementation for the closeApproachData property.
Here's a link at the Apple documentation about Encoding and Decoding Custom Types. You can find more details at the "Choose Properties to Encode and Decode Using Coding Keys" paragraph.
Also your custom class NEOCloseApproachData should be conformed to the Coding protocol.

What does .decode return in swift 4?

When you call .decode() to decode a struct, what exactly does it return?
I have look it up on the Apple Documentation, but all it says is "native format into in-memory representations." But what does this mean? Can anyone help me?
I'm asking this because my app is crashing when I get a null value from the JSON data, from this line of code:
let plantData = try decoder.decode([Plants].self, from: data)
Here is my struct:
struct Plants: Codable {
let date: String
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And Here is my Parsing code:
func parseJson() {
let url = URL(string: ebr_String)
// Load the URL
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
// If there are any errors don't try to parse it, show the error
guard let data = data, error == nil else { print(error!); return }
let decoder = JSONDecoder()
do{
let plantData = try decoder.decode([Plants].self, from: data)
print(plantData)
And Here is just a snippet of the information I am getting back:
MorrowTabbedApp.Plants(date: "2018-02-22", monthlyAVG: Optional("1210.06")), MorrowTabbedApp.Plants(date: "2018-02-23", monthlyAVG: nil)]
Here is the snippet of JSON from the web:
[
{"Date":"2018-02-21","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-22","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-23","30_Day_MA_MMBTU":null}
]
The decode method of JSONDecoder is a "generic" method. It returns an instance of whatever type you specified in the first parameter of the method. In your case, it returns a [Plants], i.e. a Array<Plants>, i.e. a Swift array of Plants instances.
If it's crashing because of a null value in your JSON, then you have to identify what was null, whether it was appropriate to be null, and if so, make sure that any Plants properties associated with values that might be null should be optionals.
Given your updated answer with code snippets, I'd suggest:
// Personally, I'd call this `Plant` as it appears to represent a single instance
struct Plant: Codable {
let date: String
let monthlyAVG: String? // Or you can use `String!` if you don't want to manually unwrap this every time you use it
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And:
do {
let plantData = try JSONDecoder().decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
Note the filter line which selects only those occurrences for which monthlyAVG is not nil.
A couple of other suggestions:
Personally, if you could, I'd rather see the web service designed to only return the values you want (those with an actual monthlyAVG) and then change the monthlyAVG property to not be an optional. But that's up to you.
If monthlyAVG is really a numeric average, I'd change the web service to not return it as a string at all, but as a number without quotes. And then change the property of Plant to be Double or whatever.
You could, if you wanted, change the date property to be a Date and then use dateDecodingStrategy to convert the string to a Date:
struct Plant: Codable {
let date: Date
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
and
do {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
let plantData = try decoder.decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
You might do this if, for example, you wanted the x-axis of your chart to actually represent time rather than an evenly spaced set of data points.

Grabbing values from a dictionary in a more elegant way

I've been playing with swift and am getting quite tortured! Consider:
var myDict : Dictionary <String, String>
//DO SOME MAGIC TO POPULATE myDict with values
<magic being done>
//Now myDict has values. Let's parse out the values of myDict
//This doesn't work
let title : String = myDict["title"]
//This does
let title : String? myDict["title"]
This is because it isn't known whether the key is in the dictionary. What I want to say, though, is "If the title key is in the dictionary, give me that value, else, just give me an empty string"
I could probably write:
var myTitle : String
if let title : String = myDict["title"] {
myTitle = title
} else {
myTitle = ""
}
I believe that works...BUT...it's quite a lot of code for EACH key of the dictionary. Does anyone have any ideas in the swift world on how this is supposed to be written?
RD
You could write an extension on optional:
extension Optional {
/// Unwrap the value returning 'defaultValue' if the value is currently nil
func or(defaultValue: T) -> T {
switch(self) {
case .None:
return defaultValue
case .Some(let value):
return value
}
}
}
Then you can do:
myDict["title"].or("")
This would also work for all optionals.
Note: I started a module to add common helpers like this or on Optional to swift.
You unwrap the value either explicitly:
let title : String = myDict["title"]!
or implicitly:
let title : String! = myDict["title"]
Note that you still have to check whether title is nil or not unless you are really sure it's there.
Edit:
Here's a sample global operator overload for any optional for type T:
#infix func | <T: Any>(lhs: T?, rhs: T!) -> T! {
if lhs {
return lhs!
}
return rhs
}
var myDict : Dictionary <String, String> = ["a": "b"]
let title1 = (myDict["a"] | "") // "b"
let title2 = (myDict["title"] | "") // ""