Usando Unbox para decodificar JSON

Usando Unbox para decodificar JSON

Grande parte dos aplicativos atuais utilizam algum tipo de informação extraída da WEB. Por padrão a maioria das fontes de dados utilizam o formato JSON (JavaScript Object Notation). Ele ele se tornou popular devido a simplicidade e a fácil leitura.

Utilizando JSON com Swift

Considere o JSON abaixo.

[{
    "menu": {
        "id": "file",
        "value": "File",
        "popup": {
            "menuitem": [{
                "value": "New",
                "onclick": "CreateNewDoc()"
            }, {
                "value": "Open",
                "onclick": "OpenDoc()"
            }, {
                "value": "Close",
                "onclick": "CloseDoc()"
            }]
        }
    }
}]

Trabalhar com JSON no Objective-C é uma tarefa simples.

NSArray *json = [NSJSONSerialization JSONObjectWithData:JSONData options:kNilOptions error:nil];

NSString *value = json[0][@"menu"][@"value"];
NSLog(@"The menu value is: %@", value);

No Swift a tarefa se torna mais trabalhosa em função dos optionals e type-safety.

var json: [[String: AnyObject]]!

do {
   json = try NSJSONSerialization.JSONObjectWithData(JSONData, options: NSJSONReadingOptions()) as? [[String: AnyObject]]
} catch {
   print(error)
}

let item = json[0]
if let menu = item["menu"] as? [String: AnyObject] {
   if let value = menu["value"] as? String {
      print("The menu value is: \(value)")
   }
}

No código acima vimos que no Swift temos que verificar os valores com optional binding o que torna o código meio confuso principalmente se a fonte de dados tiver muitos níveis.

No Swift 2.0 foi introduzido o comando guard que tornou a tarefa um pouco menos complexa retirando os ifs aninhados.

var json: [[String: AnyObject]]!

do {
   json = try NSJSONSerialization.JSONObjectWithData(JSONData, options: NSJSONReadingOptions()) as? [[String: AnyObject]]

} catch {
   print(error)
}

guard let menu = item["menu"] as? [String: AnyObject] else { return }
guard let value = menu["value"] as? String else { return }

print("The menu value is: \(value)")

Mesmo com o comando guard temos a impressão que poderia ser mais simples. Utilizei a biblioteca open source Gloss em um projeto e recentemente conheci a biblioteca Unbox que é a que iremos trabalhar hoje.

Criando o projeto

Crie um novo projeto iOS Single View Application no Xcode.

New Single View Application iOS
New Single View Application iOS

Iremos criar o projeto OpenWeatherMap utilizando Swift para iPhone.

OpenWeatherMap Project
OpenWeatherMap Project

Neste projeto iremos utilizar a fonte de dados metereológica do site OpenWeatherMap. Registre-se no site para gerar uma chave de acesso a API.

A API irá retornar um JSON com a seguinte estrutura.

{
    "coord": {
        "lon": -43.94,
        "lat": -19.92
    },
    "weather": [{
        "id": 500,
        "main": "Rain",
        "description": "chuva fraca",
        "icon": "10n"
    }],
    "base": "cmc stations",
    "main": {
        "temp": 19.77,
        "pressure": 923.85,
        "humidity": 89,
        "temp_min": 19.77,
        "temp_max": 19.77,
        "sea_level": 1026.66,
        "grnd_level": 923.85
    },
    "wind": {
        "speed": 1.51,
        "deg": 90.5001
    },
    "rain": {
        "3h": 0.355
    },
    "clouds": {
        "all": 44
    },
    "dt": 1456536694,
    "sys": {
        "message": 0.0028,
        "country": "BR",
        "sunrise": 1456563156,
        "sunset": 1456608248
    },
    "id": 3470127,
    "name": "Belo Horizonte",
    "cod": 200
}

Vamos começar o nosso projeto criando as constantes que serão utilizadas na chamada da API. Adicione a struct abaixo na classe ViewController.

private struct Constants
{
   static let APIKey = "MyAPIKey"
   static let APIAddress = "http://api.openweathermap.org/data/2.5/"
   static let CityName = "MyCity,BR"
   static let language = "pt"
   static let units = "metric"
}

Altere as propriedades APIKey e CityName com a chave gerada pelo site e pela sua localidade respectivamente.

Vamos criar a função fetchOpenWeatherMapData para consumir a API.

private func fetchOpenWeatherMapData()
{
   if let url = MakeURL() {
      let session = NSURLSession.sharedSession()

      UIApplication.sharedApplication().networkActivityIndicatorVisible = true

      let dataTask = session.dataTaskWithURL(url) { (data, response, error) in
         if let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode == 200,
            let data = data {
               dispatch_async(dispatch_get_main_queue()) {
                  UIApplication.sharedApplication().networkActivityIndicatorVisible = false

                  self.showResults(data)
               }
         } else {
            dispatch_async(dispatch_get_main_queue()) {
               UIApplication.sharedApplication().networkActivityIndicatorVisible = false

               print("Error: \(error)")
               if let data = data,
                  let dataAsString = NSString(data: data, encoding: NSUTF8StringEncoding) {
                  print("Data: \(dataAsString)")
               }
            }
         }
      }

      dataTask.resume()
   }
}

Utilizamos a classe NSURLSession para realizar a chamada da API e caso tenhamos sucesso chamaremos o método showResults a ser implementado a seguir.

Primeiramente vamos implementar o método MakeURL para gerar a URL da API.

private func MakeURL() -> NSURL?
{
   let URLString = "\(Constants.APIAddress)weather?APPID=\(Constants.APIKey)&q=\(Constants.CityName)&lang=\(Constants.language)&units=\(Constants.units)"

   if let URLString = URLString.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) {
      if let url = NSURL(string: URLString) {
         return url
      }
   }

   return nil
}

O metodo monta um objeto NSURL com os parâmetros definidos na struct Constants.

Agora vamos implementar o método showResults.

private func showResults(data: NSData)
{
   if let dataAsString = NSString(data: data, encoding: NSUTF8StringEncoding) {
      print(dataAsString)
   }
}

O metodo converte o objeto NSData recebido no método fetchOpenWeatherMapData o convertendo para String.

Rode o projeto e verifique o JSON retornado no console de log.

Utilizando a biblioteca Unbox

Para instalar a biblioteca utilizaremos o CocoaPods. Já falei sobre ele neste post.

Execute o pod init no diretório do projeto e adicione a referencia a biblioteca Unbox conforme código abaixo.

platform :ios, '8.0'
use_frameworks!

target 'Forecast' do
    pod 'Unbox'
end

Execute o pod install, abra o workspace e adicione o fonte OpenWeatherData.swift com o fonte abaixo.

import Foundation
import Unbox

class OpenWeatherData: Unboxable
{
   let clouds: Double     // Cloudiness, %
   let country: String    // Country code (GB, JP etc.)
   let city: String       // City name

   required init(unboxer: Unboxer)
   {
      self.clouds = unboxer.unbox("clouds.all", isKeyPath: true)
      self.country = unboxer.unbox("sys.country", isKeyPath: true)
      self.city = unboxer.unbox("name")
   }
}

Para instanciar um objeto com o conteúdo de um dado JSON basta implementar a interface Unboxable. Para isto vamos importar a biblioteca Unbox.

import Unbox

Implementar um construtor que recebe o objeto Unboxer.

required init(unboxer: Unboxer)
{
   self.clouds = unboxer.unbox("clouds.all", isKeyPath: true)
   self.country = unboxer.unbox("sys.country", isKeyPath: true)
   self.city = unboxer.unbox("name")
}

Para cada propriedade da classe temos que atribuir a chave correspondente no JSON.

self.city = unboxer.unbox("name")

Também podemos atribuir chaves em estruturas aninhadas utilizando pontos para compor o caminho da chave. Nestes casos devemos passar o segundo parâmetro do método unbox (isKeyPath) como true.

self.clouds = unboxer.unbox("clouds.all", isKeyPath: true)

Agora vamos modificar o método showResults para utilizar a nossa classe.

private func showResults(data: NSData)
{
   if let openWeatherData: OpenWeatherData = Unbox(data) {
      print("\(openWeatherData.clouds)")
      print("\(openWeatherData.country)")
      print("\(openWeatherData.city)")
   }
}

Além do modo optional binding utilizado acima podemos utilizar o modo error handling do Swift 2.

private func showResults(data: NSData)
{
   do {
      let openWeatherData: OpenWeatherData = try UnboxOrThrow(data)

      print("\(openWeatherData.clouds)")
      print("\(openWeatherData.country)")
      print("\(openWeatherData.city)")

   } catch UnboxError.InvalidData {
      print("Invalid data.")
   } catch UnboxError.MissingKey(let key) {
      print("MIssing key '\(key)'.")
   } catch UnboxError.InvalidValue(let key, let valueDescription) {
      print("Invalid value '\(valueDescription)' for key '\(key)'.")
   } catch UnboxError.CustomUnboxingFailed {
      print("A custom unboxing closure returned nil")
   } catch {
      print("Error")
   }
}

A biblioteca Unbox suporta os seguintes tipos de dados: Bool, Int, Double, Float, String, Array e Dictionary.

O tipo Enum também pode ser utilizado diretamente com o Unbox. Basta utilizar o protocolo UnboxableEnum.

enum WeatherIcon: String, UnboxableEnum
{
   case ClearSkyDay = "01d"
   case ClearSkyNight = "01n"
   case FewCloudsDay = "02d"
   case FewCloudsNight = "02n"
   case ScatteredCloudsDay = "03d"
   case ScatteredCloudsNight = "03n"
   case BrokenCloudsDay = "04d"
   case BrokenCloudsNight = "04n"
   case ShowerRainDay = "09d"
   case ShowerRainNight = "09n"
   case RainDay = "10d"
   case RainNight = "10n"
   case ThunderstormDay = "11d"
   case ThunderstormNight = "11n"
   case SnowDay = "13d"
   case SnowNight = "13n"
   case MistDay = "50d"
   case MistNight = "50n"

   static func unboxFallbackValue() -> WeatherIcon
   {
      return self.ClearSkyDay
   }
}

E também podemos utilizar tipos complexos com o Unbox bastando apenas implementar o protocolo Unboxable nos structs e nas classes. Vamos complementar a nossa classe OpenWeatherData.

import Foundation
import Unbox

class OpenWeatherData: Unboxable
{
   let coord: Coordinate
   let weather: [Weather] // (more info Weather condition codes)
   let base: String       // Internal parameter
   let wind: Wind
   let clouds: Double     // Cloudiness, %
   let rain: Double?      // Rain volume for the last 3 hours
   let snow: Double?      // Snow volume for the last 3 hours
   let date: Double       // Time of data calculation, unix, UTC
   let country: String    // Country code (GB, JP etc.)
   let sunrise: Double    // Sunrise time, unix, UTC
   let sunset: Double     // Sunset time, unix, UTC
   let cityId: Int        // City ID
   let city: String       // City name

   required init(unboxer: Unboxer)
   {
      self.coord = unboxer.unbox("coord")
      self.weather = unboxer.unbox("weather")
      self.base = unboxer.unbox("base")
      self.wind = unboxer.unbox("wind")
      self.clouds = unboxer.unbox("clouds.all", isKeyPath: true)
      self.rain = unboxer.unbox("rain.3h", isKeyPath: true)
      self.snow = unboxer.unbox("snow.3h", isKeyPath: true)
      self.date = unboxer.unbox("dt")
      self.country = unboxer.unbox("sys.country", isKeyPath: true)
      self.sunrise = unboxer.unbox("sys.sunrise", isKeyPath: true)
      self.sunset = unboxer.unbox("sys.sunset", isKeyPath: true)
      self.cityId = unboxer.unbox("id")
      self.city = unboxer.unbox("name")
   }

   struct Coordinate: Unboxable
   {
      let lon: Double // City geo location, longitude
      let lat: Double // City geo location, latitude

      init(unboxer: Unboxer)
      {
         self.lon = unboxer.unbox("lon")
         self.lat = unboxer.unbox("lat")
      }
   }

   struct Weather: Unboxable
   {
      let id:Int                     // Weather condition id
      let group: String              // Group of weather parameters (Rain, Snow, Extreme etc.)
      let weatherDescription: String // Weather condition within the group
      let icon: WeatherIcon          // Weather icon id

      init(unboxer: Unboxer)
      {
         self.id = unboxer.unbox("id")
         self.group = unboxer.unbox("main")
         self.weatherDescription = unboxer.unbox("description")
         self.icon = unboxer.unbox("icon")
      }
   }

   struct WeatherData: Unboxable
   {
      let temp: Double // Temperature. Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.
      let pressure: Double // Atmospheric pressure (on the sea level, if there is no sea_level or grnd_level data), hPa
      let humidity: Double // Humidity, %
      let temp_min: Double // Minimum temperature at the moment. This is deviation from current temp that is possible for large cities and megalopolises geographically expanded (use these parameter optionally). Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.
      let temp_max: Double // Maximum temperature at the moment. This is deviation from current temp that is possible for large cities and megalopolises geographically expanded (use these parameter optionally). Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.
      let sea_level: Double // Atmospheric pressure on the sea level, hPa
      let grnd_level: Double // Atmospheric pressure on the ground level, hPa

      init(unboxer: Unboxer)
      {
         self.temp = unboxer.unbox("temp")
         self.pressure = unboxer.unbox("pressure")
         self.humidity = unboxer.unbox("humidity")
         self.temp_min = unboxer.unbox("temp_min")
         self.temp_max = unboxer.unbox("temp_max")
         self.sea_level = unboxer.unbox("sea_level")
         self.grnd_level = unboxer.unbox("grnd_level")
      }
   }

   struct Wind: Unboxable
   {
      let speed: Double // Wind speed. Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour.
      let deg: Double   // Wind direction, degrees (meteorological)

      init(unboxer: Unboxer)
      {
         self.speed = unboxer.unbox("speed")
         self.deg = unboxer.unbox("deg")
      }
   }

   enum WeatherIcon: String, UnboxableEnum
   {
      case ClearSkyDay = "01d"
      case ClearSkyNight = "01n"
      case FewCloudsDay = "02d"
      case FewCloudsNight = "02n"
      case ScatteredCloudsDay = "03d"
      case ScatteredCloudsNight = "03n"
      case BrokenCloudsDay = "04d"
      case BrokenCloudsNight = "04n"
      case ShowerRainDay = "09d"
      case ShowerRainNight = "09n"
      case RainDay = "10d"
      case RainNight = "10n"
      case ThunderstormDay = "11d"
      case ThunderstormNight = "11n"
      case SnowDay = "13d"
      case SnowNight = "13n"
      case MistDay = "50d"
      case MistNight = "50n"

      static func unboxFallbackValue() -> WeatherIcon
      {
         return self.ClearSkyDay
      }
   }
}

Note o uso de arrays.

let weather: [Weather] // (more info Weather condition codes)

E de opcionais.

let rain: Double?      // Rain volume for the last 3 hours
let snow: Double?      // Snow volume for the last 3 hours

Atualizando o metodo showResults para incluir os novos campos.

private func showResults(data: NSData)
{
   let formatter = NSNumberFormatter()
   formatter.maximumFractionDigits = 2
   formatter.locale = NSLocale.currentLocale()
   formatter.numberStyle = .DecimalStyle

   do {
      let openWeatherData: OpenWeatherData = try UnboxOrThrow(data)

      print("Coordinate:\n\tLatitude: \(openWeatherData.coord.lat)\n\tLongitude: \(openWeatherData.coord.lon)")

      for w in openWeatherData.weather {
         print("Weather:")
         print("\tID: \(w.id)")
         print("\tGroup: \(w.group)")
         print("\tDescription: \(w.weatherDescription)")

         switch (w.icon) {
            case .ClearSkyDay: print("\tIcon: Clear Sky Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.ClearSkyDay.rawValue).png]")
            case .ClearSkyNight: print("\tIcon: Clear Sky Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.ClearSkyNight.rawValue).png]")
            case .FewCloudsDay: print("\tIcon: Few Clouds Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.FewCloudsDay.rawValue).png]")
            case .FewCloudsNight: print("\tIcon: Few Clouds Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.FewCloudsNight.rawValue).png]")
            case .ScatteredCloudsDay: print("\tIcon: Scattered Clouds Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.ScatteredCloudsDay.rawValue).png]")
            case .ScatteredCloudsNight: print("\tIcon: Scattered Clouds Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.ScatteredCloudsNight.rawValue).png]")
            case .BrokenCloudsDay: print("\tIcon: Broken Clouds Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.BrokenCloudsDay.rawValue).png]")
            case .BrokenCloudsNight: print("\tIcon: Broken Clouds Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.BrokenCloudsNight.rawValue).png]")
            case .ShowerRainDay: print("\tIcon: Shower Rain Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.ShowerRainDay.rawValue).png]")
            case .ShowerRainNight: print("\tIcon: Shower Rain Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.ShowerRainNight.rawValue).png]")
            case .RainDay: print("\tIcon: Rain Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.RainDay.rawValue).png]")
            case .RainNight: print("\tIcon: Rain Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.RainNight.rawValue).png]")
            case .ThunderstormDay: print("\tIcon: Thunderstorm Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.ThunderstormDay.rawValue).png]")
            case .ThunderstormNight: print("\tIcon: Thunderstorm Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.ThunderstormNight.rawValue).png]")
            case .SnowDay: print("\tIcon: Snow Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.SnowDay.rawValue).png]")
            case .SnowNight: print("\tIcon: Snow Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.SnowNight.rawValue).png]")
            case .MistDay: print("\tIcon: Mist Day [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.MistDay.rawValue).png]")
            case .MistNight: print("\tIcon: Mist Night [http://openweathermap.org/img/w/\(OpenWeatherData.WeatherIcon.MistNight.rawValue).png]")
         }
      }

      print("Base: \(openWeatherData.base)")
      print("Weather Data:")
      print("\tTemperature: \(formatter.stringFromNumber(openWeatherData.main.temp)!) °C")
      print("\tPressure: \(formatter.stringFromNumber(openWeatherData.main.pressure)!) hPa")
      print("\tHumidity: \(formatter.stringFromNumber(openWeatherData.main.humidity)!) %")
      print("\tMinimun: \(formatter.stringFromNumber(openWeatherData.main.temp_min)!) °C")
      print("\tMaximun: \(formatter.stringFromNumber(openWeatherData.main.temp_max)!) °C")
      print("\tSea Level: \(formatter.stringFromNumber(openWeatherData.main.sea_level)!) hPa")
      print("\tGround Level \(formatter.stringFromNumber(openWeatherData.main.grnd_level)!) hPa")
      print("Wind:")
      print("\tSpeed: \(formatter.stringFromNumber(openWeatherData.wind.speed)!) m/s")
      print("\tDirection: \(formatter.stringFromNumber(openWeatherData.wind.deg)!)°")
      print("Cloudiness: \(formatter.stringFromNumber(openWeatherData.clouds)!) %")

      if let rain = openWeatherData.rain {
         print("Rain: \(rain) mm")
      }

      if let snow = openWeatherData.snow {
         print("Snow: \(snow)")
      }

      print("Date: \(NSDateFormatter.localizedStringFromDate(NSDate(timeIntervalSince1970: openWeatherData.date), dateStyle: .ShortStyle, timeStyle: .ShortStyle))")
      print("Country: \(openWeatherData.country)")
      print("Sunrise: \(NSDateFormatter.localizedStringFromDate(NSDate(timeIntervalSince1970: openWeatherData.sunrise), dateStyle: .NoStyle, timeStyle: .ShortStyle))")
      print("Sunset: \(NSDateFormatter.localizedStringFromDate(NSDate(timeIntervalSince1970: openWeatherData.sunset), dateStyle: .NoStyle, timeStyle: .ShortStyle))")
      print("City ID: \(openWeatherData.cityId)")
      print("City: \(openWeatherData.city)")

   } catch UnboxError.InvalidData {
      print("Invalid data.")
   } catch UnboxError.MissingKey(let key) {
      print("MIssing key '\(key)'.")
   } catch UnboxError.InvalidValue(let key, let valueDescription) {
      print("Invalid value '\(valueDescription)' for key '\(key)'.")
   } catch UnboxError.CustomUnboxingFailed {
      print("A custom unboxing closure returned nil")
   } catch {
      print("Error")
   }
}

Verifique na pagina do GitHub do projeto sobre o uso com os tipos NSURL, e NSDate com NSDateFormatter.

Também podemos implementar a transformação em nossos próprios tipos utilizando o protocol UnboxableByTransform.

Utilizando Unbox eliminamos todos aqueles ifs aninhados e guards para popular nossos models com conteúdo JSON, deixando o código bem mais limpo e legível.

Além das bibliotecas citadas aqui (Unbox e Gloss) vale a pena dar uma olhada nas bibliotecas SwiftyJSONArgo e JSONModel para decidir qual se adapta melhor ao seu código.

Espero que tenham gostado deste post. Por favor deixe seu comentário abaixo com dúvidas, criticas e sugestões para novos posts.

Publicidade

Um comentário em “Usando Unbox para decodificar JSON

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s