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:
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))
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)
}