Instruction

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Parsing JSON Data

Parsing JSON data is one of the most fundamental skills to learn as an iOS Developer. Most apps make a network request to get data from a remote server, and once that request is executed, you’ll need to convert that data into a Swift object that can be used within your app.

struct TopResponse: Decodable {
  let id: Int

  let name: String
}
print(String(data: data, encoding: .utf8) ?? "")
do {
  let topResponse = try JSONDecoder().decode(TopResponse.self, from: data)
  print(topResponse.name)
} catch {
  print(error.localizedDescription)
}
struct WeatherCondition: Decodable {
  let id: Int
  let categoryDescription: String
  let iconString: String
}
enum CodingKeys: String, CodingKey {
  case id
  case categoryDescription = "description"
  case iconString = "icon"
}
struct TopResponse: Decodable {
  let id: Int

  let name: String

  let weatherConditions: [WeatherCondition]
}
enum CodingKeys: String, CodingKey {
  case id
  case name
  case weatherConditions = "weather"
}
print(topResponse.weatherConditions.first?.categoryDescription ?? "")
struct TemperatureData: Decodable {
  let current: Double
  let low: Double
  let high: Double

  enum CodingKeys: String, CodingKey {
    case current = "temp"
    case low = "temp_min"
    case high = "temp_max"
  }
}
struct TopResponse: Decodable {
  let id: Int

  let name: String

  let weatherConditions: [WeatherCondition]

  let temperatureData: TemperatureData

  enum CodingKeys: String, CodingKey {
    case id
    case name
    case weatherConditions = "weather"
    case temperatureData = "main"
  }
}
print(topResponse.temperatureData.current)

Handling Errors Gracefully

When you’re making networking requests and parsing JSON data, there’ll be plenty of opportunities for error. The errors can show up in many forms. The two most common categories are errors related to networking, and errors associated with decoding or parsing. To cover these scenarios, you’ll implement a NetworkError enum that contains the error cases you want to cover. These cases are somewhat arbitrary based on the scenarios the code naturally leads into whenever you hit an else or error block. Navigate to the NetworkError file and replace NetworkError with the following code:

enum NetworkError: Error {
  case invalidURL

  case invalidResponse

  case noData

  case decodeError(message: String, error: Error?)
}
func fetchWeather(for cityName: String, completion: @escaping (Result<TopResponse, NetworkError>) -> Void)
private let urlSession: URLSession
init(urlSession: URLSession = .shared) {
  self.urlSession = urlSession
}
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOUR_APP_ID_HERE") else {
    completion(.failure(.invalidURL))
    return
}
urlSession.dataTask(with: url) { data, response, _ in
  // handle response and data here.
}
DispatchQueue.main.async {
  // UI update code on the main thread here.
}
guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
  completion(.failure(NetworkError.invalidResponse))
  return
}
guard let data else {
  completion(.failure(.noData))
  return
}
do {
  let topResponse = try JSONDecoder().decode(TopResponse.self, from: data)
  completion(.success(topResponse))
} catch DecodingError.keyNotFound(let key, let context) {
  completion(
    .failure(
      .decodeError(
        message: "Could not find key \(key) in JSON: \(context.debugDescription)",
        error: nil
      )
    )
  )
} catch DecodingError.valueNotFound(let type, let context) {
  completion(
    .failure(
      .decodeError(
        message: "Could not find type \(type) in JSON: \(context.debugDescription)",
        error: nil
      )
    )
  )
} catch DecodingError.typeMismatch(let type, let context) {
  completion(
    .failure(
      .decodeError(
        message: "Type mismatch for type \(type) in JSON: \(context.debugDescription)",
        error: nil
      )
    )
  )
} catch DecodingError.dataCorrupted(let context) {
  completion(
    .failure(
      .decodeError(
        message: "Data found to be corrupted in JSON: \(context.debugDescription)",
        error: nil
      )
    )
  )
} catch {
  completion(.failure(.decodeError(message: "Generic Decoding Error", error: error)))
}
func fetchWeather(for cityName: String, completion: @escaping (Result<TopResponse, NetworkError>) -> Void) {
    guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOUR_API_KEY_HERE") else {
      completion(.failure(.invalidURL))
      return
    }
    urlSession.dataTask(with: url) { data, response, _ in
      DispatchQueue.main.async {
        guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
          completion(.failure(NetworkError.invalidResponse))
          return
        }
        guard let data else {
          completion(.failure(.noData))
          return
        }
        do {
          let topResponse = try JSONDecoder().decode(TopResponse.self, from: data)
          completion(.success(topResponse))
        } catch DecodingError.keyNotFound(let key, let context) {
          completion(
            .failure(
              .decodeError(
                message: "Could not find key \(key) in JSON: \(context.debugDescription)",
                error: nil
              )
            )
          )
        } catch DecodingError.valueNotFound(let type, let context) {
          completion(
            .failure(
              .decodeError(
                message: "Could not find type \(type) in JSON: \(context.debugDescription)",
                error: nil
              )
            )
          )
        } catch DecodingError.typeMismatch(let type, let context) {
          completion(
            .failure(
              .decodeError(
                message: "Type mismatch for type \(type) in JSON: \(context.debugDescription)",
                error: nil
              )
            )
          )
        } catch DecodingError.dataCorrupted(let context) {
          completion(
            .failure(
              .decodeError(
                message: "Data found to be corrupted in JSON: \(context.debugDescription)",
                error: nil
              )
            )
          )
        } catch {
          completion(.failure(.decodeError(message: "Generic Decoding Error", error: error)))
        }
      }
    }
    .resume()
  }

Connecting Parsed Data to UI

Up to this point, you’ve parsed the necessary models and handled the potential errors that could come up. You’re finally able to show this data via the UI. Before you update the ContentView, you’ll implement the WeatherViewModel, which will be your layer updating the user interface. Navigate to the WeatherViewModel file and add the following properties and constructor:

private var networkService: WeatherFetchable

var topResponse: TopResponse?

var errorMessage: String?

var isSearching = false

init(networkService: WeatherFetchable = NetworkService()) {
  self.networkService = networkService
}
func fetchWeather(for cityName: String) {
  isSearching.toggle()
  networkService.fetchWeather(for: cityName) { [weak self] result in
    guard let self else { return }
    switch result {
    case .success(let topResponse):
      self.topResponse = topResponse
      self.isSearching.toggle()
    case .failure(let error):
      switch error {
      case .decodeError(let message, _):
        self.errorMessage = "\(message)"
      case .invalidResponse:
        self.errorMessage = "Invalid City Name"
      case .invalidURL:
        self.errorMessage = "Invalid URL"
      case .noData:
        self.errorMessage = "No data"
      }
      self.isSearching.toggle()
    }
  }
}
private var weatherViewModel = WeatherViewModel()
NavigationStack {

}
if let topResponse = weatherViewModel.topResponse {
  // Weather data UI code here
} else {
  // Progress and error state UI here
}
VStack {
  WeatherIconView(iconString: topResponse.weatherConditions[0].iconString)
  Text(topResponse.name)
    .font(.title)
  Text(topResponse.temperatureData.current.formattedTemp)
    .font(.largeTitle)
  Text(topResponse.weatherConditions.first?.categoryDescription.capitalized ?? "")
    .font(.title3)
  HStack {
    Text("H: \(topResponse.temperatureData.high.formattedTemp)")
      .font(.headline)
    Text("L: \(topResponse.temperatureData.low.formattedTemp)")
      .font(.headline)
  }
}
if weatherViewModel.isSearching {
 ProgressView()
} else {
  VStack {
    ContentUnavailableView(
      errorMessage ?? "Please type a city above and press enter",
      systemImage: "magnifyingglass"
    )
  }
}
weatherViewModel.topResponse = nil
weatherViewModel.fetchWeather(for: searchText)
.onChange(of: weatherViewModel.errorMessage) { oldValue, newValue in
  guard oldValue != newValue else {
    return
  }
  self.errorMessage = newValue
}
See forum comments
Download course materials from Github
Previous: Introduction Next: Conclusion