Why does this JSON decoding crash my app? - swift

I have this function that is triggered by keyboard input, if the input is correct the application proceeds correctly; if not the application essentially freezes and the keyboard input cannot be changed (requires restart).
This is what calls the function:
override func viewDidLoad() {
super.viewDidLoad()
makeGetCall()
repeat{
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1))
}while !done
Function Being Called:
func makeGetCall() {
let jsonUrlString = "http://api.openweathermap.org/data/2.5/weather?q=" + city + ",us&appid=f0d10597634568abee813f68138452fd&units=imperial"
guard let url = URL(string: jsonUrlString) else {
print("Error: cannot create URL")
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
print("Error: did not receive data")
return
}
do {
self.document = try JSONDecoder().decode(WeatherDocument.self, from: data)
self.done = true
print(self.document!)
print("========================INIT===============================")
print(self.document?.main?.temp! ?? "No temp")
print(self.document?.name! ?? "No temp")
print(self.document?.weather![0].description ?? "No info")
print(self.document?.wind?.speed ?? "No wind")
print("==========================END===============================")
print(self.document?.weather![0].main ?? "No main info")
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
Any ideas why this happening?
This is the error message that is displayed in the console:
"Error serializing json: typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "cod", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil)"
WeatherDocument
struct WeatherDocument: Decodable {
let coord: Coordinates?
let weather: [Weather]?
let base: String?
let main: Main?
let visibility: Double?
let wind: Wind?
let clouds: Clouds?
let dt: Double?
let sys: Sys?
let id: Double?
let name: String?
let cod: Double?
}
Now the application does a breakpoint at the following statements I have made:
let tempe = (self.document?.main?.temp!)!
let humiditye = (self.document?.main?.humidity!)!
let pressurePow = (self.document?.main?.pressure!)! * 0.295300 * 0.10
let tempeMax = (self.document?.main?.temp_max!)! - 273.15
let tempeMin = (self.document?.main?.temp_min!)! - 273.15
//let clouding = (self.precip?.threeHours!)!
let name = (self.document?.name!)!
Why does the completion handler have a problem with this?

Two issues:
The error clearly states that the type of cod is String. And you can declare all struct members as non-optional. Openweathermap sends reliable data. There are only a few values (for example Rain in the forecast API) which are optional
let cod : String
Never use such a repeat loop. You block the thread. Use a completion handler. And it's highly recommended to use URLComponents which adds percent encoding implicitly.
var city = "New York,us"
let apiKey = <your api key>
...
func makeGetCall(completion: #escaping (WeatherDocument?, Error?)->Void) {
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")!
let queryItems = [URLQueryItem(name: "q", value: city),
URLQueryItem(name: "appid", value: apiKey),
URLQueryItem(name: "units", value: "imperial")]
guard let url = urlComponents.url else {
print("Error: cannot create URL")
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
print("Error: did not receive data")
completion(nil, error!)
}
do {
let document = try JSONDecoder().decode(WeatherDocument.self, from: data)
print(document)
print("========================INIT===============================")
print(document.main.temp) // main and temp can be non-optional
print(document.name) // name can be non-optional
print(document.weather[0].description) // weather and description can be non-optional
print(document.wind.speed) // wind and speed can be non-optional
print("==========================END===============================")
print(document.weather[0].main) // weather and main can be non-optional
completion(document, nil)
} catch {
print("Error serializing json:", error)
completion(nil, error)
}
}.resume()
}
and call it
makeGetCall() { doc, error in
if let doc = doc {
self.document = doc
} else {
print(error!)
}
}
PS: You are mixing up the forecast and the weather API. Your struct belongs to the forecast API – where cod is actually Int – but the code (and the decoding error) belongs to weather API. The struct in the question will never work with this code / URL.

The problem is this:
makeGetCall()
repeat{
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1))
} while !done
You're only calling makeGetCall() once. If it fails, you'll repeat that RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) call forever, blocking the main thread. If you want to re-attempt the makeGetCall until it works, move it down into the repeat block.
It sounds like there's also a decoding issue that will prevent makeGetCall() from completing, but I leave that for a separate answer.

Related

Does Swift task run first or print() first when I tap my UIButton?

I am trying to understand what is going on in my code here.
I have a simple API call to open weahter API and that whenever the user taps the UIButton, it should call the api and get the data back from open weather.
Everything works as intended however, when I have my UIButton pressed, the print statement executed first before the Task closure. I'm trying to understand the race condition here
This is my code in viewController:
#IBAction func callAPIButton(_ sender: UIButton) {
Task {
let weatherData = await weatherManager.fetchWeather(cityName: "Seattle")
}
}
Here's the code for fetching the API:
struct WeatherManager{
let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=someAPIKeyHere"
func fetchWeather(cityName: String) -> WeatherModel? {
let urlString = "\(weatherURL)&q=\(cityName)"
let requestResult = performRequest(urlString: urlString)
return requestResult
}
func performRequest(urlString: String) -> WeatherModel? {
var weatherResult : WeatherModel? = nil
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error != nil {
return
}
if let safeData = data {
weatherResult = parseJSON(weatherData: safeData)
}
})
task.resume()
}
return weatherResult
}
func parseJSON(weatherData: Data) -> WeatherModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(decodedData)")
let temp = decodedData.main.temp
let name = decodedData.name
let weather = WeatherModel(conditionId:300, cityName: name, temperature: temp)
return weather
} catch {
print("Something is wrong here: " + error.localizedDescription)
}
return nil
}
}
Here's my Model:
struct WeatherModel{
let conditionId: Int
let cityName: String
let temperature: Double
var temperatureString: String{
return String(format: "%.1f", temperature)
}
var conditionName: String {
switch conditionId {
case 200...232:
return "cloud.bolt"
case 300...321:
return "cloud.drizzle"
case 500...531:
return "cloud.rain"
case 600...622:
return "cloud.snow"
case 701...781:
return "cloud.fog"
case 800:
return "sun.max"
case 801...804:
return "cloud.bolt"
default:
return "cloud"
}
}
}
Desired result:
This is in weatherData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e+09, sunset: 1.6713243e+09))
This is what I am getting instead:
This is in weatherData: nil
this is in decodedData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e+09, sunset: 1.6713243e+09))
Thank you in advance
Everything works as intended
No, it doesn't. I don't know why you claim such a thing; your code isn't working at all.
The problem is that you are trying to return weatherResult from performRequest. But performRequest gets its weatherResult value asynchronously, so this attempt is doomed to failure; you will always be returning nil, because the return weatherResult happens before session.dataTask ever even starts to find out what weatherResult is.
You cannot just synchronously return the results of an asynchronous request. You have two basic options for asynchronous requests.
Use the older “completion handler” pattern with Result types:
struct WeatherManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
let appId = "someAPIKeyHere"
func fetchWeather(
cityName: String,
completion: #escaping (Result<WeatherModel, Error>) -> Void
) {
guard var components = URLComponents(string: weatherURL) else {
completion(.failure(URLError(.badURL)))
return
}
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "q", value: cityName)
]
guard let url = components.url else {
completion(.failure(URLError(.badURL)))
return
}
performRequest(url: url, completion: completion)
}
func performRequest(
url: URL,
queue: DispatchQueue = .main,
completion: #escaping (Result<WeatherModel, Error>) -> Void
) {
let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
let task = session.dataTask(with: url) { data, response, error in
guard
error == nil,
let data = data,
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
queue.async { completion(.failure(error ?? URLError(.badServerResponse))) }
return
}
do {
let weatherResult = try parseJSON(weatherData: data)
queue.async { completion(.success(weatherResult)) }
} catch {
queue.async { completion(.failure(error)) }
}
}
task.resume()
}
func parseJSON(weatherData: Data) throws -> WeatherModel {
let decoder = JSONDecoder()
let response = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(response)")
return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
}
}
Then, rather than:
let weather = weatherManager.fetchWeather(cityName: …)
You would
weatherManager.fetchWeather(cityName: …) { result in
switch result {
case .failure(let error):
print(error)
case .success(let weather):
// do something with the `weather` object here
}
}
// note, do not do anything with `weather` here, because the above
// runs asynchronously (i.e., later).
Use the newer async-await pattern of Swift concurrency:
struct WeatherManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
let appId = "someAPIKeyHere"
func fetchWeather(cityName: String) async throws -> WeatherModel {
guard var components = URLComponents(string: weatherURL) else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "q", value: cityName)
]
guard let url = components.url else {
throw URLError(.badURL)
}
return try await performRequest(url: url)
}
func performRequest(url: URL) async throws -> WeatherModel {
let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
let (data, response) = try await session.data(from: url)
guard
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
throw URLError(.badServerResponse)
}
return try parseJSON(weatherData: data)
}
func parseJSON(weatherData: Data) throws -> WeatherModel {
let decoder = JSONDecoder()
do {
let response = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(response)")
return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
} catch {
print("Something is wrong here: " + error.localizedDescription)
throw error
}
}
}
And then you can do things like:
Task {
do {
let weather = try await weatherManager.fetchWeather(cityName: …)
// do something with `weather` here
} catch {
print(error)
}
}
Note, a few changes in the above unrelated to the asynchronous nature of your request:
Avoid creating URLSession instances. If you do, you need to remember to invalidate them. Instead, it is much easier to use URLSession.shared, eliminating this annoyance.
Avoid building URLs with string interpolation. Use URLComponents to build safe URLs (e.g., ones that can handle city names like “San Francisco”, with spaces in their names).

How to struct's value such that all views can access its values in Swift/SwiftUI?

Currently I am decoding a JSON response from an API and storing it into a struct "IPGeolocation". I want to be able to store this data in a variable or return an instance of this struct such that I can access the values in views.
Struct:
struct IPGeolocation: Decodable {
var location: Coordinates
var date: String
var sunrise: String
var sunset: String
var moonrise: String
var moonset: String
}
struct Coordinates: Decodable{
var latitude: Double
var longitude: Double
}
URL extension with function getResult:
extension URL {
func getResult<T: Decodable>(completion: #escaping (Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: self) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error!))
return
}
do {
completion(.success(try data.decodedObject()))
} catch {
completion(.failure(error))
}
}.resume()
}
}
Function that retrieves and decodes the data:
func getMoonTimes(lat: Double, long: Double) -> Void{
urlComponents.queryItems = queryItems
let url = urlComponents.url!
url.getResult { (result: Result<IPGeolocation, Error>) in
switch result {
case let .success(result):
print("Printing returned results")
print(result)
case let .failure(error):
print(error)
}
}
}
My goal is to take the decoded information and assign it to my struct to be used in views after. The results variable is already an IPGeolocation struct once the function runs. My question lies in the best way to store it or even return it if necessary.
Would it make sense to have getResult return an IPGeolocation? Are there better/different ways?
Thanks!
EDIT: I made changes thanks to help from below comments from Leo Dabus.
func getMoonTimes(completion: #escaping (IPGeolocation?,Error?) -> Void) {
print("STARTING FUNC")
let locationViewModel = LocationViewModel()
let apiKey = "AKEY"
let latitudeString:String = String(locationViewModel.userLatitude)
let longitudeString:String = String(locationViewModel.userLongitude)
var urlComponents = URLComponents(string: "https://api.ipgeolocation.io/astronomy?")!
let queryItems = [URLQueryItem(name: "apiKey", value: apiKey),
URLQueryItem(name: "lat", value: latitudeString),
URLQueryItem(name: "long", value: longitudeString)]
urlComponents.queryItems = queryItems
urlComponents.url?.getResult { (result: Result<IPGeolocation, Error>) in
switch result {
case let .success(geolocation):
completion(geolocation, nil)
case let .failure(error):
completion(nil, error)
}
}
}
To call this method from my view:
struct MoonPhaseView: View {
getMoonTimes(){geolocation, error in
guard let geolocation = geolocation else {
print("error:", error ?? "nil")
return
}
}
...
...
...
IMO it would be better to return result and deal with the error. Note that you are assigning the same name result which you shouldn't. Change it to case let .success(geolocation). What you need is to add a completion handler to your method because the request runs asynchronously and return an option coordinate and an optional error as well:
Note that I am not sure if you want to get only the location (coordinates) or the whole structure with your method. But the main point is to add a completion handler to your method and pass the property that you want or the whole structure.
func getMoonTimes(for queryItems: [URLQueryItem], completion: #escaping (Coordinates?,Error?) -> Void) {
urlComponents.queryItems = queryItems
urlComponents.url?.getResult { (result: Result<IPGeolocation, Error>) in
switch result {
case let .success(geolocation):
completion(geolocation.location, nil)
case let .failure(error):
completion(nil, error)
}
}
}
Usage:
getMoonTimes(for: queryItems) { location, error in
guard let location = location else {
print("error:", error ?? "nil")
return
}
print("Location:", location)
print("Latitude:", location.latitude)
print("Longitude:", location.longitude)
}

Swift: Multithreading Issue Getting Data From API

I'm having a problem where sometimes I can't successfully decode a json file due to a slow networking call. I have a list of stocks and I'm trying to successfully decode each one before the next stock gets decoded.
For example, I would have a list of 4 stocks but 3 would be successful decoded but 1 won't. The one that fails is also random. When I print out the url for the one that fails, the url and json file is correct yet I get an error of it not reading because its on a wrong format.
The list of stocks are retrieved through Firebase and after I receive them, I have a completion handler that tries to make a network call to the server. The reason why I added Firestore code here is because when I put a stop point at when case is successful, I notice that its hitting Thread 5 out of 14 Threads. Is having this many threads common? I know it's a threading issue but am having such a huge problem identifying where I should do dispatchGroups. Any help and clarifications would be much appreciated!
APIManager
private var stocks = [Stock]()
func getStockList( for symbols: [Stock], completion: ((Result<[Stock]>) -> Void)?) {
let dispatchGroup = DispatchGroup()
for symbol in symbols {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = APIManager.baseAPIURL
urlComponents.path = "\(APIManager.baseRelativePath)/market/batch"
//URL parameters
let token = URLQueryItem(name: "token", value: "fakeToken")
let symbolsItem = URLQueryItem(name: "symbols", value: symbol.symbol)
let typesItem = URLQueryItem(name: "types", value: "quote")
urlComponents.queryItems = [token, symbolsItem, typesItem]
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
let session = URLSession(configuration: config)
dispatchGroup.enter()
let task = session.dataTask(with: request) { (responseData, response, err) in
guard err == nil else {
completion?(.failure(err!))
return
}
guard let jsonData = responseData else {
let err = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(err))
return
}
let decoder = JSONDecoder()
do {
let data = try decoder.decode([String: Stock].self, from: jsonData)
let jsonData = data.map{ $0.value }
completion?(.success(jsonData))
dispatchGroup.leave()
} catch {
completion?(.failure(error))
print("Failed to decode using stock URL for \(symbol.symbol ?? ""): \n \(url)")
}
}
task.resume()
}
}
HomeViewController
class HomeViewController: UIViewController, LoginViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
fetchStocksFromFireStore(completion: { (stocks) -> Void in
APIManager.shareInstance.getStockList(for: self.fetchedStocks) { (result) in
switch result {
case .success(let stocks):
stocks.forEach { (stock) in
print("Company: \(stock.companyName ?? "")")
}
case .failure(let error):
print(error.localizedDescription)
}
}
self.tableView.reloadData()
})
}
}
func fetchStocksFromFireStore(completion: #escaping ([Stock]) -> ()) {
let uid = Auth.auth().currentUser?.uid ?? ""
let db = Firestore.firestore()
db.collection("users").document(uid).collection("stocks").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting stocks snapshot", err.localizedDescription)
return
} else {
snapshot?.documents.forEach({ (snapshot) in
var stock = Stock()
stock.symbol = snapshot.documentID
self.fetchedStocks.append(stock)
})
completion(self.fetchedStocks)
}
}
}
Model
struct Stock {
var symbol: String?
var companyName: String?
var latestPrice: Double?
enum CodingKeys: String, CodingKey {
case symbol
case companyName
case latestPrice
}
enum QuoteKeys: String, CodingKey {
case quote
}
}
extension Stock: Encodable {
func encode(to encoder: Encoder) throws {
var quoteContainer = encoder.container(keyedBy: QuoteKeys.self)
var quoteNestedValues = quoteContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .quote)
try quoteNestedValues.encode(symbol, forKey: .symbol)
try quoteNestedValues.encode(companyName, forKey: .companyName)
try quoteNestedValues.encode(latestPrice, forKey: .latestPrice)
}
}
extension Stock: Decodable {
init(from decoder: Decoder) throws {
let quoteValues = try decoder.container(keyedBy: QuoteKeys.self)
let quoteNestedValues = try quoteValues.nestedContainer(keyedBy: CodingKeys.self, forKey: .quote)
symbol = try quoteNestedValues.decode(String.self, forKey: .symbol)
companyName = try quoteNestedValues.decode(String.self, forKey: .companyName)
latestPrice = try quoteNestedValues.decode(Double.self, forKey: .latestPrice)
}
}

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

Swift unit testing: async

I'm having trouble getting my unit tests right, I want to check it the data is in my struct, I can get my function to print out how many items there are however the test fails because the expectation doesn't come back.
I know I need to use
XCTAssertNotNil(self.players.countPlayers)
heres my function title
public func getPlayer(completionHandler: #escaping (Data?, URLResponse?, Error?) -> (Swift.Void)
) throws{
let jsonUrl: String = "http://api.football-data.org/v1/teams/78/players"
//print(jsonUrl)
// NSURL sessions allow us to download data using HTTP for APIs
// a NSURL which contains a correct resourse
guard let leagueURL = NSURL(string: jsonUrl)else{
print("error creating string")
throw JSONError.InvalidURL(jsonUrl)
}
let task = URLSession.shared.dataTask(with: leagueURL as URL) {data, responce, error in
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
self.searchLeague = []
for item in json["players"] as! [[String: AnyObject]]{
guard let name = ((item["name"]!) as? String) else{
throw JSONError.InvalidKey("invalid player name")
}
guard let position = ((item["position"]!) as? String) else{
throw JSONError.InvalidKey("invalid player position")
}
let jerseyNumber = ( ((item["jerseyNumber"]!) as? String) ?? ("No Assigned jersey number") )
guard let dateOfBirth = ((item["dateOfBirth"]!) as? String) else{
throw JSONError.InvalidKey("invalid player DOB")
}
guard let nationality = ((item["nationality"]!) as? String) else{
throw JSONError.InvalidKey("invalid player DOB")
}
let marketvalue = ( ((item["marketValue"]!) as? String) ?? ("Market value info not available"))
self.searchPlayer.append(Player(name: name, position: position, jerseyNumber: jerseyNumber, dateOfBirth: dateOfBirth, nationality: nationality, marketValue: marketvalue))
}
}catch{
print("error thrown: \(error)")
}
print("Num of Players \(self.searchPlayer.count)")
}
task.resume()
}
and my Test
func testGetPlayers(){
let expectations = expectation(description: "Wait for exception")
try! self.players.getPlayer{_,_,_ in
expectations.fulfill()
}
waitForExpectations(timeout: 5) { error in
}
}
You've successfully demonstrated why unit testing is a good idea.
You've found a serious bug in your getPlayer method - you never call the completion handler.
You should be calling the completionHandler closure with the appropriate arguments from various places within your getPlayer method depending on whether you successfully process the data or encounter an error.