Initialising 'top' struct in Swift - swift

I'm currently attempting to initialise a struct with Swift, that goes several structs deep. Once an external API is called, it sends back data which I want to place into a struct so that I can use it to populate a TableView.
I'm wanting to decode data that looks like this:
{
"data": {
"1": {
"name": "Bitcoin",
"quote": {
"GBP": {
"price": 25794.72142905233,
"percent_change_1h": -1.4133929,
"percent_change_24h": -0.74636982,
"percent_change_7d": -5.8533249
}
}
},
"52": {
"name": "XRP",
"quote": {
"GBP": {
"price": 0.7157097479533718,
"percent_change_1h": -1.35513268,
"percent_change_24h": 1.84172355,
"percent_change_7d": 5.05130272
}
}
}
}
To do this, I have the following Struct structure:
Coin struct which references data
Datum struct which references the '1' and '52'
Quote struct which references the 'quote'
GBP struct which references the 'price', 'percent_change_1h'
These look like this:
struct Coin: Codable {
let data: [String: Datum]
}
struct Datum: Codable {
let name: String
let quote: Quote
}
struct Quote: Codable {
let gbp: Gbp
enum CodingKeys: String, CodingKey {
case gbp = "GBP"
}
}
struct Gbp: Codable {
let price, percentChange1H, percentChange24H, percentChange7D: Double
enum CodingKeys: String, CodingKey {
case price
case percentChange1H = "percent_change_1h"
case percentChange24H = "percent_change_24h"
case percentChange7D = "percent_change_7d"
}
}
I'm then attempting to set up a variable which is an empty 'Coin' struct under the variable name 'coins' like so:
var coins = Coin()
Unfortunately - trying to do this prompts the following error:
Missing argument for parameter 'data' in call
If I then follow the prompts, I can insert the following:
var coins = Coin(data: [String : Datum])
But this then produces the following error:
Cannot convert value of type '[String: Datum].Type' to expected
argument type '[String: Datum]'
Please can somebody point out what is going wrong here? Have I built my structs incorrectly? Is there an alternative I can do?

Rather than [String : Datum], which the compiler is telling you is a Type, use [:], which is the Swift syntax for an empty Dictionary:
var coins = Coin(data: [:])

Related

How to deal with empty array to be sent as json in swift?

I'm trying to encode data but struggling to deal with empty array with no type as the API's I am working with needs me to send [] if there is no entry.
[
{
"RequestId": "5B6E36D9-8759-41BB-A0C0-EDFB116DFBB7",
"DataSources": ["5B6E36D9-8759-41BB-A0C0-EDFB116DFBB7"],
"GroupBy": [],
"Filters": []
}
]
above is the object json which I have to send.
struct ResponseElement: Encodable {
let requestID: String
let dataSources: [String]
let groupBy: []
let filters: []
enum CodingKeys: String, CodingKey {
case requestID = "RequestId"
case dataSources = "DataSources"
case groupBy = "GroupBy"
case filters = "Filters"
}
}
let data = ResponseElement(requestID: "5B6E36D9-8759-41BB-A0C0-EDFB116DFBB7",
dataSources: ["5B6E36D9-8759-41BB-A0C0-EDFB116DFBB7", ["5B6E36D9-8759-41BB-A0C0-EDFB116DFBB7]"],
groupBy: [],
filters: [])
let jsonEncoder = JSONEncoder()
let data = try! jsonEncoder.encode(data)
please note while creating data variable I have to pass groupBy, filters as empty array [], I have tried with [nil] which goes as [null] after encoding but it doesn't work in my case, it has to be []
how do I solve this please?
If you know that the array will always be empty, the type of that is [Never]. A Never is a type that cannot be instantiated, so this array is necessarily empty.
Unfortunately, Never does not conform to Encodable. It should. It should conform to every protocol automatically (that would make Never a "bottom type," which is a good feature in a type system). That said, it's straightforward to provide the conformance:
extension Never: Encodable {
public func encode(to encoder: Encoder) throws {
// This line of code can never, ever run. It's impossible to
// create a Never, so it's impossible to run its instance methods.
fatalError()
}
}
struct ResponseElement: Encodable {
let requestID: String
let dataSources: [String]
let groupBy: [Never]
let filters: [Never]
...
}
You may rightly feel uncomfortable extending a stdlib type to conform with a stdlib protocol. This is kind of fragile, since stdlib might create the conformance in the future, or some other package might do it, and there'd be a conflict. So you can do this explicitly by creating a type of your own that encodes an empty unkeyed container:
struct EmptyArray: Encodable {
func encode(to encoder: Encoder) throws {
encoder.unkeyedContainer()
}
}
struct ResponseElement: Encodable {
let requestID: String
let dataSources: [String]
let groupBy: EmptyArray
let filters: EmptyArray
...
}
And finally, you can perform the encoding by hand and get rid of the unnecessary properties (this is how I'd do it myself):
struct ResponseElement: Encodable {
let requestID: String
let dataSources: [String]
enum CodingKeys: String, CodingKey {
case requestID = "RequestId"
case dataSources = "DataSources"
case groupBy = "GroupBy"
case filters = "Filters"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(requestID, forKey: .requestID)
try container.encode(dataSources, forKey: .dataSources)
container.nestedUnkeyedContainer(forKey: .groupBy)
container.nestedUnkeyedContainer(forKey: .filters )
}
}
If this is just for encoding data and you never have to provide anything for this properties you could use any type you want for your property:
e.g.
struct ResponseElement: Encodable {
let requestID: String
let dataSources: [String]
let groupBy: [String] = []
let filters: [String] = []
enum CodingKeys: String, CodingKey {
case requestID = "RequestId"
case dataSources = "DataSources"
case groupBy = "GroupBy"
case filters = "Filters"
}
}
Result:
{
"RequestId": "1",
"GroupBy": [],
"Filters": [],
"DataSources": [
"1",
"2"
]
}

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"
}

Access Data Object by ID

I have the following structs that defined according to the SwiftUI example in Apple.
struct Cat: Hashable, Codable, Identifiable {
let id: Int
let name: String
let audio: [Int]? // list of audio of cat sound
}
struct CatAudio: Hashable, Codable, Identifiable {
let id: Int
let filename: String
}
I then would like to access the audio and then deliver in the view.
I have json data like this:
[
{
"id": 55,
"name": "meow",
"audio": [6,5]
},
{
"id": 3,
"name": "meowmeow",
"audio": [2]
}
]
AudioData.json
[
{
"id": 5,
"filename": "5.wav"
},
{
"id": 2,
"filename": "2.wav"
},
{
"id": 6,
"filename": "6.wav"
}
]
The json files loaded successfully.
#Published var cats: [Cat] = load("CatData.json")
#Published var catAudios: [CatAudio] = load("CatAudio.json")
I then tried to get an audio object from my environment model data:
#EnvironmentObject var modelData: ModelData
and then I want to get an the corresponding audio object of the cat. but I failed to do so as I do not know how to use the "Id" to get it.
Example:
Assume that I got the cat object from my model:
let cat = modelData.cats[0]
I then want to get its audio data according to the id stored in the audio list of it
let catAudio = modelData.catAudios[cat.audio[0]!] // exception here
I found that it is because the array order may not be consistent with the "Id". I want to make use of the "Id" instead of the Array order to get the item.
How can I do it?
========
I have tried to write a function to get the list of CatAudio of a cat.
I have also make audio non-optional.
func getAudio(cat: Cat) -> [CatAudio] {
return cat.audio.compactMap{id in catAudio.filter { $0.id==id } }
}
But it complains and said that cannot convert the value of type [CatAudio] to closure result type 'CatAudio'
I got confused with that.
You need to use high-order functions to match the id values in the audio array with the elements in the CatAudio array
Assuming the 2 arrays and a selected Cat object
var cats: [Cat] = ...
var catAudios: [CatAudio] = ...
let cat = cats[0]
To select one audio for a one id
if let firstId = cat.audio?.first {
let audio = catAudios.first { $0.id == firstId}
}
To get an array of all CatAudio for the cat object
if let array = cat.audio {
let values = array.compactMap { id in catAudios.filter { $0.id == id }}
}
The code would be simpler if the audio array wasn't optional, any reason for it to be declared optional?

Type of expression is ambiguous without more context in Xcode 11

I'm trying to refer to an [Item] list within an #EnvironmentObject however when accessing it within a SwiftUI List, I get the error. What I don't understand is, this error doesn't pop up when following Apple's Landmark tutorial.
As far as I can tell, the [Item] list is loading correctly as I can print it out and do other functions with it. It just bugs out when using it for a SwiftUI List Is there something I've missed?
ItemHome.swift:
struct ItemHome : View {
#EnvironmentObject var dataBank: DataBank
var body: some View {
List {
ForEach(dataBank.itemList) { item in
Text("\(item.name)") // Type of expression is ambiguous without more context
}
}
}
}
Supporting code below:
Item Struct:
struct Item {
var id: Int
var uid: String
var company: String
var item_class: String
var name: String
var stock: Int
var average_cost: Decimal
var otc_price: Decimal
var dealer_price: Decimal
var ctc_price: Decimal
}
DataBank.swift:
final class DataBank : BindableObject {
let didChange = PassthroughSubject<DataBank, Never>()
var itemList: [Item] = load("itemsResults.json") {
didSet {
didChange.send(self)
}
}
}
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
itemsResults.json:
[
{
"id": 1,
"uid": "a019bf6c-44a2-11e9-9121-4ccc6afe39a1",
"company": "Bioseed",
"item_class": "Seeds",
"name": "9909",
"stock": 0,
"average_cost": 0.0,
"otc_price": 0.0,
"dealer_price": 0.0,
"ctc_price": 0.0
},
{
"id": 2,
"uid": "a019bf71-44a2-11e9-9121-4ccc6afe39a1",
"company": "Pioneer",
"item_class": "Seeds",
"name": "4124YR",
"stock": 0,
"average_cost": 0.0,
"otc_price": 0.0,
"dealer_price": 0.0,
"ctc_price": 0.0
}
]
Apparently I missed making sure my models (Item in this case) conformed to the Identifiable protocol fixed it. Still, I wish Apple was more clear with their error messages.
As you mentioned in your answer, a ForEach needs a list of Identifiable objects. If you don't want to make your object implement that protocol (or can't for some reason), however, here's a trick:
item.identifiedBy(\.self)
I had the same problem and it wasn't something related to the line itself, it was related to the curly braces/brackets, so that if someone faced the same problem and doesn't know where the problem is, try to trace the curly braces and the brackets
To conform to Identifiable, just give the id or uid variable a unique value.
An easy way to do this is this:
var uid = UUID()
So your full struct would be:
struct Item: Identifiable {
var id: Int
var uid = UUID()
var company: String
var item_class: String
var name: String
var stock: Int
var average_cost: Decimal
var otc_price: Decimal
var dealer_price: Decimal
var ctc_price: Decimal
}
Xcode may show this error in many cases. Usually when using higher order functions (like map) and reason may be "anything".
There are two types:
Error is in higher order function. In this case your only option is to carefully read the block of code. Once again there may be different kinds of problems.
In some cases higher order function does not have any problem by itself, but there may be a compile time error in function that's called from the body of the higher order function.
Unfortunately Xcode does not point to this error sometimes.
To detect such errors and save lot of time, easy workaround is to temporarily comment higher order function and try to build. Now Xcode will show this error function and will show more reasonable error.

How do I correctly print a struct?

I'm trying to store an array of store structs within my users struct, but I can't get this to print correctly.
struct users {
var name: String = ""
var stores: [store]
}
struct store {
var name: String = ""
var clothingSizes = [String : String]()
}
var myFirstStore = store(name: "H&M", clothingSizes: ["Shorts" : "Small"])
var mySecondStore = store(name: "D&G", clothingSizes: ["Blouse" : "Medium"])
var me = users(name: "Me", stores: [myFirstStore, mySecondStore])
println(me.stores)
You’re initializing them just fine. The problem is your store struct is using the default printing, which is an ugly mangled version of the struct name.
If you make it conform to CustomStringConvertible, it should print out nicely:
// For Swift 1.2, use Printable rather than CustomStringConvertible
extension Store: CustomStringConvertible {
var description: String {
// create and return a String that is how
// you’d like a Store to look when printed
return name
}
}
let me = Users(name: "Me", stores: [myFirstStore, mySecondStore])
println(me.stores) // prints "[H&M, D&G]"
If the printing code is quite complex, sometimes it’s nicer to implement Streamable instead:
extension Store: Streamable {
func writeTo<Target : OutputStreamType>(inout target: Target) {
print(name, &target)
}
}
p.s. convention is to have types like structs start with a capital letter