How to use a value from a struct dictionary and convert it to another type? - swift

Here I want to be able to use the value returned from an array. It returns as a type from a struct. I'm unsure of how to use the value as an integer.
struct Item: Codable {
let data: [String : Datum]
}
struct Datum: Codable {
let value: Int
}
var array = Item(data: ["1" : Datum(value: 1),"2": Datum(value: 2), "3":Datum(value: 3)])
var keyArray = ["1", "2", "3"]
print(array.data[keyArray[0]]!)
// Prints Datum(value: 1)
print(array.data[keyArray[0]]! + 1)
//This produces an error "Cannot convert value of type 'Datum' to expected argument type 'Int'"
//Expected result should be 2
My use case is when I get returned a decoded JSON it normally comes back as a dictionary. I'm wanting to use the values returned with a key but I feel like I'm one step short.
Context
Full JSON Link
I'm going to retrieve values from this JSON. (Example from large JSON file)
{"data":{"2":{"high":179,"highTime":1628182107,"low":177,"lowTime":1628182102},"6":{"high":189987,"highTime":1628179815,"low":184107,"lowTime":1628182100},"8":{"high":190800,"highTime":1628181435,"low":188100,"lowTime":1628182095}
}}
The string in front refers to an item ID.
The struct that I came up to decode goes like this.
// MARK: - Single
struct Single: Codable {
let data: [String: Datum]
}
// MARK: - Datum
struct Datum: Codable {
let high, highTime: Int
let low, lowTime: Int?
}
From there I'm planning to iterate through the JSON response to retrieve the item prices I'd want.
#available(iOS 15.0, *)
struct ContentView: View {
#State var dataFromURL: Single = Single(data: [:])
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
async {
try await decode()
}
}
}
func decode() async throws -> Single {
let decoder = JSONDecoder()
let urlString = "https://prices.runescape.wiki/api/v1/osrs/latest"
guard let url = URL(string: urlString) else { throw APIError.invalidURL }
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw APIError.invalidServerResponse }
guard let result = try? decoder.decode(Single.self, from: data) else { throw APIError.invalidData }
//We copy our result to an existing variable
dataFromURL = result
return result
}
}
enum APIError: Error {
case invalidURL
case invalidServerResponse
case invalidData
}
extension APIError: CustomStringConvertible {
public var description: String {
switch self {
case.invalidURL:
return "Bad URL"
case .invalidServerResponse:
return "The server did not return 200"
case .invalidData:
return "Their server returned bad data"
}
}
}
I haven't gotten further than grabbing the response from the URL. That is why once I start manipulating the data I'd like to use the response to find other things like what would a profit/loss with another item become. Which isn't the goal of this question at the moment.

The object model to parse that JSON would be:
struct Price: Decodable {
let high: Int?
let highTime: Date?
let low: Int?
let lowTime: Date?
}
struct ResponseObject: Decodable {
let prices: [String: Price]
enum CodingKeys: String, CodingKey {
case prices = "data"
}
}
(Note, the documentation says that either high or low might be missing, so we have to make them all optionals.)
Now, the id number is being passed as a string in the JSON/ResponseObject. But that is a number (look at mapping). So, I would remap that dictionary so that the key was an integer, e.g.
enum ApiError: Error {
case unknownError(Data?, URLResponse?)
}
func fetchLatestPrices(completion: #escaping (Result<[Int: Price], Error>) -> Void) {
let url = URL(string: "https://prices.runescape.wiki/api/v1/osrs/latest")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
completion(.failure(error ?? ApiError.unknownError(data, response)))
return
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
do {
let responseObject = try decoder.decode(ResponseObject.self, from: responseData)
let keysAndValues = responseObject.prices.map { (Int($0.key)!, $0.value) }
let prices = Dictionary(uniqueKeysWithValues: keysAndValues)
completion(.success(prices))
} catch {
completion(.failure(error))
}
}
task.resume()
}
The code that converts that [String: Price] to a [Int: Price] is this:
let keysAndValues = responseObject.prices.map { (Int($0.key)!, $0.value) }
let prices = Dictionary(uniqueKeysWithValues: keysAndValues)
I must say that this is a questionable API design, to have keys returned as integers in one endpoint and as strings as another. But it is what it is. So, the above is how you handle that.
Anyway, now that you have a dictionary of prices, keyed by the id numbers, you can use that in your code, e.g.
var prices: [Int: Price] = [:]
var products: [Product] = []
let group = DispatchGroup()
group.enter()
fetchLatestPrices { result in
defer { group.leave() }
switch result {
case .failure(let error):
print(error)
case .success(let values):
prices = values
}
}
group.enter()
fetchProducts { result in
defer { group.leave() }
switch result {
case .failure(let error):
print(error)
case .success(let values):
products = values }
}
group.notify(queue: .main) {
for product in products {
print(product.name, prices[product.id] ?? "no price found")
}
}
Where
func fetchProducts(completion: #escaping (Result<[Product], Error>) -> Void) {
let url = URL(string: "https://prices.runescape.wiki/api/v1/osrs/mapping")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
completion(.failure(error ?? ApiError.unknownError(data, response)))
return
}
do {
let products = try JSONDecoder().decode([Product].self, from: responseData)
completion(.success(products))
} catch {
completion(.failure(error))
}
}
task.resume()
}
And
struct Product: Decodable {
let id: Int
let name: String
let examine: String
let members: Bool
let lowalch: Int?
let limit: Int?
let value: Int
let highalch: Int?
let icon: String
}
(As an aside, I do not know if some of these other properties should be optionals or not. I just used optionals where I empirically discovered that they are occasionally missing.)

Related

Array vs Dictionary response structures with JSONDecoder

Got the following data model:
class ResponseMultipleElements<Element: Decodable>: Decodable {
let statuscode: Int
let response_type: Int
let errormessage: String?
let detailresponse: Element?
}
class Element<T: Decodable>: Decodable {
let count: String;
let element: T?
}
For the following API response structure:
{
"statuscode": 200,
"response_type": 3,
"errormessage": null,
"detailresponse": {
"count": "1",
"campaigns": [
{
"id": 1,
"name": "Foo",
"targetagegroup": null,
"creator":...
...
}
}
}
I'm triggering JSONDecoder like this:
class APIService: NSObject {
func getCampaignList(completion: #escaping(Result<[Campaign], APIError>) -> Void) {
guard let endpoint = URL(string: apiBaseUrlSecure + "/campaignlist") else {fatalError()}
var request = URLRequest(url: endpoint)
request.addValue("Bearer " + UserDefaults.standard.string(forKey: "authtoken")!, forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let jsonData = data
else { print("ERROR: ", error ?? "unknown error"); completion(.failure(.responseError)); return }
do {
let response = try JSONDecoder().decode(ResponseMultipleElements<[Campaign]>.self, from: jsonData)
completion(.success(response.detailresponse!))
} catch {
print("Error is: ", error)
completion(.failure(.decodingError))
}
}
dataTask.resume()
}
...
}
And I'm finally trying to make use of the decoded campaign object like this
class CoopOverviewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
//do stuff
// load Campaigns
self.apiService.getCampaignList(completion: {result in
switch result {
case .success(let campaigns):
DispatchQueue.main.async {
print("CAMPAIGN DATA: ", campaigns[0].name)
}
case .failure(let error):
print("An error occured \(error.localizedDescription)")
}
})
...
}
Now I've got 2 questions:
1)
let element: T?
is actually called "campaigns" in the api response for this call. However, it could be cooperations, payments, etc. in other api responses with that same ResponseMultipleElements surrounding structure. Is there a way to make the key swappable here, like I've done with the value with the use of generics? If not, how else would I solve that problem?
2) I'm getting this error:
typeMismatch(Swift.Array<Any>,
Swift.DecodingError.Context(codingPath:
[CodingKeys(stringValue: "detailresponse", intValue: nil)],
debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))
I've told Swift that the "campaigns" part of the detailresponse is an Array of campaign objects - at least that's my understanding when looking at the api response. However, the error seems to say it's a dictionary. First, I don't get why that is and would really like to understand it. Second, I don't know how to tell it that it should expect a dictionary instead of an array then - getting confused with generics here a bit.
Thank you so much for your help in advance!
This is an approach to add a custom key decoding strategy to map any CodingKey but count in detailresponse to fixed value element.
First of all create a custom CodingKey
struct AnyCodingKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) {
return nil
}
}
Then create the structs similar to Sh_Khan's answer, in most cases classes are not needed
struct ResponseMultipleElements<T: Decodable>: Decodable {
let statuscode : Int
let response_type : Int
let errormessage : String?
let detailresponse : Element<T>
}
struct Element<U: Decodable>: Decodable {
let count : String
let element : U
}
struct Campaign : Decodable {
let id : Int
let name : String
let targetagegroup : String?
}
Now comes the funny part. Create a custom key decoding strategy which returns always element for the CodingKey in detailresponse which is not count
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom { codingKeys in
let lastKey = codingKeys.last!
if lastKey.intValue != nil || codingKeys.count != 2 { return lastKey }
if lastKey.stringValue == "count" { return lastKey }
return AnyCodingKey(stringValue: "element")!
}
let result = try decoder.decode(ResponseMultipleElements<[Campaign]>.self, from: data)
completion(.success(result.detailresponse.element))
} catch {
print("Error is: ", error)
completion(.failure(error))
}

Access a struct within a struct to return a sorted array

I am parsing data to my Users object from a JSON url and attempting to return the object sorted alphabetically by names. I am having trouble accessing the names property inside of my Users object. I have nested my struct inside of a struct because of the way my JSON is structured. I would like to return the array sorted alphabetically by names in my sortedList array.
struct Response: Codable
{
struct Users: Codable {
var fullName :String
var biography:String
enum CodingKeys: String, CodingKey {
case fullName = "full_name"
case biography
}
}
var users:[Users]
}
// let sortedList = Response{ $0.fullName < $1.fullName }
//Cannot invoke initializer for type 'Response' with an argument list of type '((_, _) -> _)'
func parse(){
guard let url = URL(string: "samplePage.json") else {return}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let dataResponse = data,
error == nil else {
print(error?.localizedDescription ?? "Error")
return }
do{
let response = try! JSONDecoder().decode(Response.self, from: dataResponse)
for user in response.users{
print(user.fullName)
print(user.biography)
let sortedArr = user.fullName.sorted{ $0.fullName < $1.fullName }
//Value of type 'Character' has no member 'fullName'
}
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
}
For sorting purpose code use like this :
let sortedArr = response.users.sorted{ $0.fullName < $1.fullName }
no need for for loop.
updated code:
func parse(){
guard let url = URL(string: "samplePage.json") else {return}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let dataResponse = data,
error == nil else {
print(error?.localizedDescription ?? "Error")
return }
do{
let response = try! JSONDecoder().decode(Response.self, from: dataResponse)
for user in response.users{
print(user.fullName)
print(user.biography)
}
// use code like this
let sortedArr = response.users.sorted{ $0.fullName < $1.fullName }
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
}

Decoding dynamic JSON structure in swift 4

I have the following issue that I'm not sure how to handle.
My JSON response can look like this:
{
"data": {
"id": 7,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDY1MTU0NDMsImRhdGEiOiJ2bGFkVGVzdCIsImlhdCI6MTU0NjUwODI0M30.uwuPhlnchgBG4E8IvHvK4bB1Yj-TNDgmi7wUAiKmoVo"
},
"error": null
}
Or like this:
{
"data": [{
"id": 12
}, {
"id": 2
}, {
"id": 5
}, {
"id": 7
}],
"error": null
}
So in short the data can be either a single objet or an Array. What i have is this:
struct ApiData: Decodable {
var data: DataObject?
var error: String?
}
struct DataObject: Decodable {
var userId: Int?
enum CodingKeys: String, CodingKey {
case userId = "id"
}
}
This works fine for the first use case, but it will fail once data turns into
var data: [DataObject?]
How do I make that dynamic without duplicating code?
Edit: This is how i decode the object as well
func makeDataTaskWith(with urlRequest: URLRequest, completion: #escaping(_ apiData: ApiData?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: urlRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
if let responseCode = response as? HTTPURLResponse {
print("Response has status code: \(responseCode.statusCode)")
}
do {
let retreived = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(retreived)
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
}
If data can be a single object or an array write a custom initializer which decodes first an array, if a type mismatch error occurs decode a single object. data is declared as an array anyway.
As token appears only in a single object the property is declared as optional.
struct ApiData: Decodable {
let data : [DataObject]
let error : String?
private enum CodingKeys : String, CodingKey { case data, error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
data = try container.decode([DataObject].self, forKey: .data)
} catch DecodingError.typeMismatch {
data = [try container.decode(DataObject.self, forKey: .data)]
}
error = try container.decodeIfPresent(String.self, forKey: .error)
}
}
struct DataObject: Decodable {
let userId : Int
let token : String?
private enum CodingKeys: String, CodingKey { case userId = "id", token }
}
Edit: Your code to receive the data can be improved. You should add a better error handling to return also all possible errors:
func makeDataTaskWith(with urlRequest: URLRequest, completion: #escaping(ApiData?, Error?) -> Void) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: urlRequest) {
(data, response, error) in
if let error = error { completion(nil, error); return }
if let responseCode = response as? HTTPURLResponse {
print("Response has status code: \(responseCode.statusCode)")
}
do {
let retreived = try NetworkManager.shared.decoder.decode(ApiData.self, from: data!)
completion(retreived, nil)
} catch {
print("Decoder error: ", error)
completion(nil, error)
}
}.resume()
}
Using power of generic, it simple like below:
struct ApiData<T: Decodable>: Decodable {
var data: T?
var error: String?
}
struct DataObject: Decodable {
private var id: Int?
var userId:Int? {
return id
}
}
Use
if let obj = try? NetworkManager.shared.decoder.decode(ApiData<DataObject>.self, from: data) {
//Do somthing
} else if let array = try NetworkManager.shared.decoder.decode(ApiData<[DataObject]>.self, from: data) {
// Do somthing
}
If you have only two possible outcomes for your data, an option would be to try and parse data to one of the expected types, if that fails you know that the data is of other type and you can then handle it accordingly.
See this
You can try
struct Root: Codable {
let data: DataUnion
let error: String?
}
enum DataUnion: Codable {
case dataClass(DataClass)
case datumArray([Datum])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([Datum].self) {
self = .datumArray(x)
return
}
if let x = try? container.decode(DataClass.self) {
self = .dataClass(x)
return
}
throw DecodingError.typeMismatch(DataUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for DataUnion"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .dataClass(let x):
try container.encode(x)
case .datumArray(let x):
try container.encode(x)
}
}
}
struct Datum: Codable {
let id: Int
}
struct DataClass: Codable {
let id: Int
let token: String
}
let res = try? JSONDecoder().decode(Root.self, from:data)

Swift 4 Codable - API provides sometimes an Int sometimes a String

I have Codables running now. But the API has some String entries that can sometimes have an Int value of 0 if they are empty. I was searching here and found this: Swift 4 Codable - Bool or String values But I'm not able to get it running
My struct
struct check : Codable {
let test : Int
let rating : String?
}
Rating is most of the time something like "1Star". But if there is no rating I get 0 as Int back.
I'm parsing the data like this:
enum Result<Value> {
case success(Value)
case failure(Error)
}
func checkStar(for userId: Int, completion: ((Result<check>) -> Void)?) {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "xyz.com"
urlComponents.path = "/api/stars"
let userIdItem = URLQueryItem(name: "userId", value: "\(userId)")
urlComponents.queryItems = [userIdItem]
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Authorization": "Bearer \(keytoken)"
]
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonData = responseData {
// Now we have jsonData, Data representation of the JSON returned to us
// from our URLRequest...
// Create an instance of JSONDecoder to decode the JSON data to our
// Codable struct
let decoder = JSONDecoder()
do {
// We would use Post.self for JSON representing a single Post
// object, and [Post].self for JSON representing an array of
// Post objects
let posts = try decoder.decode(check.self, from: jsonData)
completion?(.success(posts))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
Loading it:
func loadStars() {
checkStar(for: 1) { (result) in
switch result {
case .success(let goo):
dump(goo)
case .failure(let error):
fatalError(error.localizedDescription)
}
}
}
I hope someone can help me there, cause I'm not completely sure how this parsing, etc. works.
you may implement your own decode init method, get each class property from decode container, during this section, make your logic dealing with wether "rating" is an Int or String, sign all required class properties at last.
here is a simple demo i made:
class Demo: Decodable {
var test = 0
var rating: String?
enum CodingKeys: String, CodingKey {
case test
case rating
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let test = try container.decode(Int.self, forKey: .test)
let ratingString = try? container.decode(String.self, forKey: .rating)
let ratingInt = try? container.decode(Int.self, forKey: .rating)
self.rating = ratingString ?? (ratingInt == 0 ? "rating is nil or 0" : "rating is integer but not 0")
self.test = test
}
}
let jsonDecoder = JSONDecoder()
let result = try! jsonDecoder.decode(Demo.self, from: YOUR-JSON-DATA)
if rating API's value is normal string, you will get it as you wish.
if rating API's value is 0, rating will equal to "rating is nil or 0"
if rating API's value is other integers, rating will be "rating is integer but not 0"
you may modify decoded "rating" result, that should be easy.
hope this could give you a little help. :)
for more info: Apple's encoding and decoding doc

Using decoded data from an API into an algorithm

I successfully fetched and decoded data from an API and now have access to all the data I need to be used in the algorithm I want to write in my App.
The issue is that I don't know how to access this data after I decoded it, I can print it immediately after it's decoded but I have no idea how to use it in another function or place in my app.
Here is my Playground:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError : Error {
case FoundNil(String)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
print(marketData.data[0].open)
print(marketData.data[1].open)
print("Average=", (marketData.data[0].open + marketData.data[1].open) / 2)
//completion(marketData, nil)
throw MyError.FoundNil("data")
}
} catch {
print(error)
}
}
task.resume()
}
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
How can I use .data[0], .data[1], ..., somewhere else?
You data will be available in your fecthData() call. Probably what you want is your items variable, where you're printing it. But make sure to call the completion in your fetchData implementation.
WARNING: Untested code.
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError: Error {
case FoundNil(String)
case DecodingData(Data)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
completion(nil, MyError.FoundNil("data"))
}
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
completion(marketData, nil)
} else {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
} catch {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
}
task.resume()
}
fetchData() { items, error in
if let error == error {
switch(error) {
case .foundNil(let whatsNil):
print("Something is nil: \(whatsNil)")
case .decodingData(let data):
print("Error decoding: \(data)")
}
} else {
if let items = items {
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
print(items)
} else {
print("No items to show!")
}
}
}
I don't understand what is your real issue, because you have written everything you need here, but as far I understand , to pass data
just uncomment this line completion(marketData, nil)
and in
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
items is an object of your struct Response. You can pass this anywhere in your other class , by just creating an another variable like:
var items : Response!
for example :
class SomeOtherClass : NSObject{
var items : Response!
func printSomeData()
{
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
}
}
and in fetchData method write this:
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
let otherObject = SomeOtherClass()
otherObject.items = items
otherObject.printSomeData()
}