Search code examples
iosswiftxcode

Xcode - API Failing When Search Size is Greater Than 14


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
}

Solution

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