When fetching data from the iTunes API https://itunes.apple.com/search?term=\(search)&entity=software&limit=14, the fetch fails if the limit is a larger number (e.g. 30, 40, 50 etc.). The limit is denoted by limit=14 found at the end of the URL. 14 is the number of results returned. This can be changed to any number.
When making the call in Postman, I can enter the limit as any number and it works without error. Additionally, when running the api with a large number in XCtest, the test passes. It only seems to fail when making the call live in the app.
The failure occurs in the guard let statement. In the code below, if the number is too large (e.g. 50), it prints "failed to fetch data" - indicating that there is a URL issue. When using a smaller number (e.g. 10), the fetch is successful and data returns in my table view. You can also change the search term. Currently I have it set to "Apple".
Below is the code for the API:
import Foundation
import UIKit
struct Response: Codable {
var resultCount: Int
var results: [Result]
}
struct Result: Codable {
var screenshotUrls, ipadScreenshotUrls, appletvScreenshotUrls: [String]
var artworkUrl60, artworkUrl512, artworkUrl100, artistViewUrl: String
var supportedDevices, advisories: [String]
var isGameCenterEnabled: Bool
var features: [String]
var kind, minimumOsVersion, trackCensoredName, fileSizeBytes: String
var contentAdvisoryRating: String
var genreIds: [String]
var primaryGenreName, artistName, trackContentRating, trackName, releaseDate, sellerName, currentVersionReleaseDate, releaseNotes, version: String
var primaryGenreId: Int
var currency, description: String
var price: Double
var averageUserRating: Double
}
class API {
var storedData = Response(resultCount: Int.init(), results: [])
func loadData(search: String, completionHandler: #escaping (Response) -> Void) {
guard let url = URL(string:"https://itunes.apple.com/search?term=\(search)&entity=software&limit=40") else {
print("failed to fetch data")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.storedData.resultCount = response.resultCount
self.storedData.results = response.results
completionHandler(self.storedData)
}
return
}
}
print("failed \(error?.localizedDescription ?? "unknown error")")
}
.resume()
}
func reloadTableData() {
DataManager.shared.viewController.tableView.reloadData()
}
}
Any thoughts as to why a larger number causes the guard let to fail when running the app, but does not fail in my tests or postman?
EDIT
Below is how I am calling the function. I am calling it in viewdidload. It uses a completion handler, so it looks like the following:
api.loadData(search: "ibm") { Results in
self.filteredResults = self.api.storedData.results //stores value in filtered results array
self.tableView.reloadData() //refreshes table view - table view is referencing the filteredResults array
}
There are many keys missing in the JSON, when you set a limit of more than 20:
Always use doCatch when decoding JSON and print the error, which tells you what went wrong, in your source decoding is failing because of the following error: refer
keyNotFound(CodingKeys(stringValue: "releaseNotes", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "results", intValue: nil), _JSONKey(stringValue: "Index 15", intValue: 15)], debugDescription: "No value associated with key CodingKeys(stringValue: \"releaseNotes\", intValue: nil) (\"releaseNotes\").", underlyingError: nil))
Try making all variables in the struct Optional to fix decoding issue for now.
Make properties optional
struct Response: Codable {
var resultCount: Int?
var results: [Result]?
}
struct Result: Codable {
var screenshotUrls, ipadScreenshotUrls, appletvScreenshotUrls: [String]?
var artworkUrl60, artworkUrl512, artworkUrl100, artistViewUrl: String?
var supportedDevices, advisories: [String]?
var isGameCenterEnabled: Bool?
var features: [String]?
var kind, minimumOsVersion, trackCensoredName, fileSizeBytes: String?
var contentAdvisoryRating: String?
var genreIds: [String]?
var primaryGenreName, artistName, trackContentRating, trackName, releaseDate, sellerName, currentVersionReleaseDate, releaseNotes, version: String?
var primaryGenreId: Int?
var currency, description: String?
var price: Double?
var averageUserRating: Double?
}
Put this inside the if-let data block:
do {
let response = try JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
self.storedData.resultCount = response.resultCount
self.storedData.results = response.results
completionHandler(self.storedData)
}
} catch let error {
print(error)
}
Related
My Firestore data is set up like this:
This is how I'm reading the data:
for doc in snapshot!.documents {
let recipeFromFirestore = Recipe(
glutenFree: doc["glutenFree"] as! Bool,
dairyFree: doc["dairyFree"] as! Bool,
cheap: doc["cheap"] as! Bool)
recipes.append(recipeFromFirestore)
}
These are my Recipe and ExtendedIngredients structs:
struct Recipe: Codable {
var glutenFree: Bool?
var dairyFree: Bool?
var cheap: Bool?
var extendedIngredients: [ExtendedIngredients]? = nil
}
struct ExtendedIngredients: Codable {
var aisle: String?
var image: String?
var name: String?
var amount: Double?
var unit: String?
}
How can I go about reading the array of Map type data in my extendedIngredients field in Firestore? I'm not sure how to include that in my let recipeFromFirestore code.
Any help or guidance is much appreciated!
I was able to get all of my data, including the Map type by using the Codable API.
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.recipe = try document.data(as: Recipe.self)
let recipeFromFirestore = Recipe(
glutenFree: self.recipe!.glutenFree,
dairyFree: self.recipe!.dairyFree,
cheap: self.recipe!.cheap,
extendedIngredients: self.recipe!.extendedIngredients)
self.recipes.append(recipeFromFirestore)
}
catch {
print("Line 136: \(error)")
}
}
}
}
I did not need to do any explicit mapping with this approach.
I'm trying to make a stocks app for college related work and ive done almost everything i need to do except getting the actual data of the stocks into my app, I've been trying this and researching for the past couple days but still cannot get it to work as i get the error message :
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))
any help is appreciated as this is the first ever thing i've made with swift/xcode
the code in my viewController:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchPostData { (posts) in
for post in posts {
print(post.datetime!)
}
}
}
func fetchPostData(completionHandler: #escaping ([Post]) -> Void) {
let url = URL(string: "https://api.twelvedata.com/time_series?symbol=AAPL&interval=1min&apikey=<api-key>")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let postsData = try JSONDecoder().decode([Post].self, from: data)
completionHandler(postsData)
}
catch {
let error = error
print(error)
}
}.resume()
}
}
and the other file with the variables:
struct Post: Codable {
var datetime: Int!
var open: Int!
var high: String!
var low: String!
var close: String!
var volume: String!
}
link to the json file: https://api.twelvedata.com/time_series?symbol=AAPL&interval=1min&apikey=0561a81a9baf4ae4bc65c7af9196f929
The error is speaking for itself; you are trying to decode an array but the JSON is a dictionary. You need to change your decode:
JSONDecoder().decode(Post.self, from: data)
Edit after Joakim Danielson hint for completeness of the answer:
You have also to modify your struct in order to accomodate the JSON in your response
struct Post: Decodable {
let meta: Meta
}
struct Meta: Decodable {
let symbol: String
let interval: String
let currency: String
let values: [Values]
// more data here
}
struct Values: Decodable {
// more data here
}
First of all, all values in Post are String, please note the double quotes in the JSON
struct Post: Decodable {
let datetime, open, high, low, close, volume: String
}
But the main issue is – as mentioned in the comments and Alastar's answer and indirectly stated by the error – you are ignoring the root object. You have to decode JSON always from the top. Add this struct
struct Root: Decodable {
let status: String
let values: [Post]
}
and decode
let postsData = try JSONDecoder().decode(Root.self, from: data)
completionHandler(postsData.values)
I am trying to fetch the Covid 19 data of all countries and their states from disease.sh.
I have previously fetched json data from different APIs using this method. The response in those cases were shorter compared to this.
I have posted the codes below:
// Webservice.swift
import Foundation
class Webservice {
let countriesURL: String = "https://disease.sh/v3/covid-19/jhucsse"
func getAllCountries(completion: #escaping ([Country]?) ->()) {
guard let url = URL(string: countriesURL) else {
completion(nil)
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("No data in response: \(error?.localizedDescription ?? "Unknown error").")
DispatchQueue.main.async {
completion(nil)
}
return
}
let countries = try? JSONDecoder().decode([Country].self, from: data)
DispatchQueue.main.async {
countries == nil ? completion(nil) : completion(countries)
}
}.resume()
}
}
Since, I was using MVVM design, here are my Model, ViewModel and View.
// Model
// Country.swift
import Foundation
struct Country: Decodable {
var country: String
var updatedAt: String
var stats: Stats
var coordinates: Coordinates
var province: String
}
struct Stats: Decodable {
var confirmed: Int
var deaths: Int
var recovered: Int
}
struct Coordinates: Decodable {
var latitude: String
var longitude: String
}
// ViewModel
// CountryListViewModel.swift
import Foundation
class CountryListViewModel: ObservableObject {
#Published var countries = [CountryViewModel]()
init() {
fetchCountries()
}
func fetchCountries() {
Webservice().getAllCountries() { countries in
if let countries = countries {
self.countries = countries.map(CountryViewModel.init)
}
}
}
}
class CountryViewModel {
var country: Country
init(country: Country) {
self.country = country
}
let id = UUID()
var name: String {
return self.country.country
}
var updatedAt: String {
return self.country.updatedAt
}
var stats: Stats {
return self.country.stats
}
var coordinates: Coordinates {
return self.country.coordinates
}
var province: String {
return self.country.province
}
}
// View
// ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject private var countryListVM = CountryListViewModel()
var body: some View {
List( self.countryListVM.countries, id:\.id) { country in
Text(country.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
My issue is when I call the Webservice.getAllCountries() it returns nil. Could anyone look at the code and tell me what is wrong, please? Thank you!
PS: I created a mock json with fewer objects (20-30) and called Webservice.getAllCountries() in this case it returned and mapped the values. It is not working with larger JSON response. Help!!
You should avoid using try ? except in situations where you really don't care about failures. do/try/catch is a better approach since it will tell you why something failed.
Changing your code to
do {
let countries = try JSONDecoder().decode([Country].self, from: data)
DispatchQueue.main.async {
completion(countries)
}
} catch {
print(error)
completion(nil)
}
Gives us an error on the console -
Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 509", intValue: 509), CodingKeys(stringValue: "province", intValue: nil)], debugDescription: "Expected String value but found null instead.", underlyingError: nil))
which makes sense, since not all countries have provinces. To fix this, make province an optional in your data model
struct Country: Decodable {
var country: String
var updatedAt: String
var stats: Stats
var coordinates: Coordinates
var province: String?
}
I am having trouble decoding JSON Data in swift after using an API call. The application is actually a Pokedex application. I am having trouble; specifically on the line where I initialize the variable flavorTexts. I have found that I have received data from the URL (checkout https://pokeapi.co/) to view the pokemon data. When I print this data it comes out as x number of bytes. But after I decode it into flavorTexts and I print flavorTexts, nothing prints meaning flavorTexts has nothing in it. I have also found that the DispatchQueue function does not even begin as I can't any prints inside it do not print. Can someone help me? The third set of code is the data structs I have created for these API calls. For the API call I am having trouble with the structs used are: descriptions, items, and language.
The error I am getting is:
keyNotFound(CodingKeys(stringValue: "lang", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "flavor_text_entries", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"lang\", intValue: nil) (\"lang\").", underlyingError: nil))
API CALL:
func loadDescription () {
url2 = "https://pokeapi.co/api/v2/pokemon-species/" + (name) + "/"
urlReal = URL(string: url2)
print(url2!)
URLSession.shared.dataTask(with: urlReal) { (data1, response, error) in
guard let data1 = data1 else {
return
}
print(data1)
do {
//This is where I am having trouble
let flavorTexts = try JSONDecoder().decode(descriptions.self, from: data1)
// DispatchQueue.main.async {
// var fte = flavor_text_entry.flavor_text_entries
// for ft in fte {
// if ft.lang.name == "en" {
// self.textDescription.text = ft.flavor_text
// break
// }
// }
// }
}
catch {
error
}
}.resume()
}
Data Structs:
import Foundation
struct PokemonListResults: Codable {
let results: [PokemonListResult]
}
struct PokemonListResult: Codable {
let name: String
let url: String
}
struct PokemonResult: Codable {
let id: Int
let name: String
let types: [PokemonTypeEntry]
}
struct PokemonTypeEntry: Codable {
let slot: Int
let type: PokemonType
}
struct PokemonType: Codable {
let name: String
}
struct PokemonImage: Codable {
let sprites: sprite
}
struct sprite: Codable {
let front_default: String
}
struct descriptions: Codable {
let flavor_text_entries: [items]
}
struct items: Codable {
let flavor_text: String
let lang: language
}
struct language: Codable {
let name: String
}
I am trying to parse some json but seem to be getting nil in the outputs.
I am not sure where I am going wrong and could use some help trying to figure this out.
struct albumInfo: Decodable {
var name: String?
var artist: String?
var url: String?
var playcount: String?
var listeners: String?
var releasedate: String?
var summary: String?
}
class SearchVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Choice = "album"
Album = "Believe"
Artist = "Cher"
let tryURL = "\(BASE_URL)\(Choice!).getinfo&api_key=\(API_KEY)&artist=\(Artist!)&album=\(Album!)&format=json"
print(tryURL)
guard let url = URL(string: tryURL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let albumDescription = try JSONDecoder().decode(albumInfo.self, from: data)
print(albumDescription.artist)
}catch let jsonErr {
print("Error seroalizing json", jsonErr)
}
}.resume()
}
Here is the data as shown with the tryUrl.
First of all please conform to the naming convention that struct names start with a capital letter.
There are two major issues:
The root object is a dictionary with one key album containing the dictionary with keys name, listeners etc.
The key summary is in another dictionary for key wiki.
The structure of the JSON is very easy to identify. The body within each pair of braces ({}) represents a separate struct.
Further there is no key releasedate so this struct member has to be declared as optional, all other members can be declared as non-optional and as constants (let). url can be declared as URL for free.
Change your structs to
struct Root : Decodable {
let album : AlbumInfo
}
struct AlbumInfo: Decodable {
let name: String
let artist: String
let url: URL
let playcount: String
let listeners: String
let releasedate: String?
let wiki : Wiki
}
struct Wiki: Decodable {
let content: String
let published: String
let summary: String
}
and decode Root
let albumDescription = try JSONDecoder().decode(Root.self, from: data)
print(albumDescription.album.artist)
The first key of your response is "album", you need to parse that first.
The classes do not correspond to json, I guess you should use the following approach (new classes implement your decode, encode protocol):
class JsonInfo {
var album : albumInfo
}
do {
let albumDescription = try JSONDecoder().decode(albumInfo.self, from: data)
print(albumDescription.album.artist)
}catch let jsonErr {
print("Error seroalizing json", jsonErr)
}