Search code examples
iosswiftstructnull

Issues with Swift valueNotFound during API Call


I am looking for some help working with a Swift project. I am building an app that pulls aviation weather data from an API and this situation is common:

User wants data from airport weather station KDAB - current report says:

  • Wind 10 kt
  • Scattered Clouds 2500 ft
  • Visibility 10 SM
  • Light Rain

User wants data from airport weather station KJAX - current report says:

  • Wind 16 kt
  • Wind Gust 24 kt
  • Broken Clouds 1400 ft
  • Scattered Clouds 1900 ft
  • Few clouds 2400 ft

In this simple example, you may notice that there is no wind gust data supplied for KJAX during this reporting period and no "special weather" (ie rain, haze, fog) specified for KDAB. My app needs to be able to handle "nil" or not provided data without just telling me that there is a valueNotFound or that the index is out of range.

Here are the API Docs: https://avwx.docs.apiary.io/#reference/0/metar/get-metar-report

Here is my code:

import Foundation

struct WeatherManager {
    let weatherURL = "https://avwx.rest/api/metar/"

    func fetchWeather (stationICAO: String) {
        let urlString = "\(weatherURL)\(stationICAO)?token=OVi45FiTDo1LmyodShfOfoizNe5m9wyuO6Mkc95AN-c"
        performRequest(urlString: urlString)
    }
    
    func performRequest (urlString: String) {
        if let url = URL(string: urlString) {
            let session = URLSession(configuration: .default)
                
            
            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    print(error!)
                    return
                }
                
                if let safeData = data {
                    self.parseJSON(weatherData: safeData)
                }
            }
            
            task.resume()
            print(urlString)
            
            
            }
        }
    
    func parseJSON(weatherData: Data) {
        
        
        do {
            let decoder = JSONDecoder()
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            
            
            
            let lowCloudsType = decodedData.clouds[0].type
            let midCloudsType = decodedData.clouds[1].type 
            let highCloudsType = decodedData.clouds[2].type 
            let lowCloudsAlt = decodedData.clouds[0].altitude
            let midCloudsAlt = decodedData.clouds[1].altitude 
            let highCloudsAlt = decodedData.clouds[2].altitude 
            let reportingStationVar = decodedData.station 
            let windGustValue = decodedData.wind_gust.value 
            let windSpeedValue = decodedData.wind_speed.value 
            let windDirectionValue = decodedData.wind_direction.value 
            let visibilityValue = decodedData.visibility.value
            let flightRulesValue = decodedData.flight_rules
            
            let weather = WeatherModel(lowestCloudsType: lowCloudsType, lowestCloudsAlt: lowCloudsAlt, middleCloudsType: midCloudsType, middleCloudsAlt: midCloudsAlt, highestCloudsType: highCloudsType, highestCloudsAlt: highCloudsAlt, reportingStation: reportingStationVar, windGust: windGustValue, windSpeed: windSpeedValue, windDirection: windDirectionValue, visibility: visibilityValue, flightRules: flightRulesValue)
            
            print(weather.flightConditions)
            
        } catch {
            print(error)
        }
    }
    
    

}    
import Foundation

struct WeatherModel {
        
    let lowestCloudsType: String
    let lowestCloudsAlt: Int
    let middleCloudsType: String
    let middleCloudsAlt: Int
    let highestCloudsType: String
    let highestCloudsAlt: Int
    let reportingStation: String
    let windGust: Int
    let windSpeed: Int
    let windDirection: Int
    let visibility: Int
    let flightRules: String
    
    var flightConditions: String {
        switch flightRules {
        case "VFR":
            return "green"
        case "MVFR":
            return "blue"
        case "IFR":
            return "red"
        case "LIFR":
            return "purple"
        default:
            return "gray"
        
        }
    }
}

Last One:

import Foundation

struct WeatherData: Decodable {
   
    
    let clouds: [Clouds]
    let flight_rules: String
    let remarks: String
    let wind_speed: WindSpeed
    let wind_gust: WindGust
    let wind_direction: WindDirection
    let visibility: Visibility

    let station: String
}



struct Clouds: Decodable {
    let type: String
    let altitude: Int
}

struct WindSpeed: Decodable {
    let value: Int
}

struct WindGust: Decodable {
    let value: Int
}

struct WindDirection: Decodable {
    let value: Int
}

struct Visibility: Decodable {
    let value: Int
}

Depending on what I play with, I get the following errors when entering a station that does not have all of that given information that I need to be able to present to the user if reported by the weather service.

2020-09-22 02:47:58.930421-0400 AvWx Pro[66612:4483807] libMobileGestalt MobileGestaltCache.c:38: No persisted cache on this platform.
KDAB
https://avwx.rest/api/metar/KDAB?token=(mySecretToken)
2020-09-22 02:48:02.943231-0400 AvWx Pro[66612:4483809] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
valueNotFound(Swift.KeyedDecodingContainer<AvWx_Pro.WindGust.(unknown context at $1053fb3b8).CodingKeys>, 
Swift.DecodingError.Context(codingPath: 
[CodingKeys(stringValue: "wind_gust", intValue: nil)], 
debugDescription: "Cannot get keyed decoding container 
-- found null value instead.", underlyingError: nil))

A different error when I use an airport that doesn't have all three of the possible cloud layers reported:

2020-09-22 03:06:02.398628-0400 AvWx Pro[66736:4497432] libMobileGestalt MobileGestaltCache.c:38: No persisted cache on this platform.
KJAX
https://avwx.rest/api/metar/KJAX?token=(mySecretKey)
2020-09-22 03:06:07.955064-0400 AvWx Pro[66736:4497429] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
Fatal error: Index out of range: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1200.2.22.2/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444
2020-09-22 03:06:08.908826-0400 AvWx Pro[66736:4497429] Fatal error: Index out of range: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1200.2.22.2/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444
(lldb) 

I've spent a few hours now trying various solutions I've found online, including using optionals and force unwrapping, using guard let, using if let, and a few others. I'm pretty lost at the moment.

I am new to this platform (as a poster) and would really appreciate any insight anyone can offer!


Solution

  • To avoid crashes while decoding json you should make correct configuration of your structs and check the fields before access values:

    1. Optional fields

    [CodingKeys(stringValue: "wind_gust", intValue: nil)], debugDescription: "Cannot get keyed decoding container -- found null value instead.", underlyingError: nil)) wind_gust can be empty so you should make it optional:

    If a field can be empty or null in responses you should make it optional in your struct e.g.:

    struct WeatherData: Decodable { 
        ...
        let wind_gust: WindGust?
        ...
    }
    

    Then in your code just use optional binding to extract a value if wind_gust exists:

        if let value = decodedData.wind_gust?.value {
            print(value)
        }
    
    1. Arrays

    Fatal error: Index out of range

    You must always check an array's bounds before access to items e.g.:

    let clouds = decodedData.clouds
    let lowCloudsType = clouds.count > 0 ? clouds[0].type : nil
    let midCloudsType = clouds.count > 1 ? clouds[1].type : nil
    let highCloudsType = clouds.count > 2 ? clouds[2].type : nil
    
    if let low = lowCloudsType, let mid = midCloudsType, let high = highCloudsType {
        print(low)
        print(mid)
        print(high)
    }