Conectando o app iOS

Nessa prática vamos completar o desenvolvimento do App Countries, tal como fizemos com sua versão para Android, para obter a listagem completa dos países, bem como exibir suas bandeiras na listagem e na tela de detalhes.

Configurando Permissões para Sites não HTTP/S

Apesar de o iOS ter um mecanismo de permissões bastante completo, de maneira semelhante ao Android, ele não chega ao nível de granularidade de acesso em rede, sendo que, por padrão, qualquer app pode contar com essa capacidade. No entanto, a partir do iOS 9 foi incluído um mecanismo de segurança chamado App Transport Security, forçando que toda comunicação feita através dos apps se conecte exclusivamente com servidores HTTP/S.

O objetivo da Apple é contribuir com a diminuição do uso do HTTP sem SSL/TLS, que é considerado extremamente inseguro, graças à natureza completamente aberta das comunicações entre cliente e servidor.

No entanto, o serviço GeoNames trabalha exclusivamente sobre HTTP, portanto vamos precisar configurar nosso App de forma a desabilitar o App Transport Security. Isso é feito incluindo uma chave no arquivo de configuração Info.plist. Ao abrir esse arquivo por padrão é apresentado o editor visual:

Porém, o arquivo do tipo plist nada mais é do um XML em uma formatação específica criada pela Apple. O que desejamos é editar seu código fonte diretamente, pulando o uso do editor visual. Para isso, clique com o botão direito no arquivo no Solution Navigator, conforme ilustrado abaixo:

Isso fará com que o arquivo seja aberto no editor de código padrão.

Dica: você pode usar esse mesmo caminho para abrir outros arquivos de diferentes formas. Por exemplo, os Storyboards também são arquivos XML e você talvez tenha curiosidade em ver seu código-fonte.

Antes do fechamento da última tag </dict> inclua o código abaixo:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

Com isso seu arquivo de configuração deverá ficar semelhante ao ilustrado abaixo:

Importante: a Apple prevê que a partir de janeiro de 2017, não serão mais aceitas submissões de Apps que desativem o App Transport Security. Casos excepcionais deverão ser solicitados justificando o motivo de não utilizarem HTTP/S. Lembre-se disso caso esteja criando um App conectado.

Adaptando o Modelo do App

Tal como no Android, toda a parte de comunicação com o servidor e tratamento do retorno, será por conta do modelo. Nesse exercício, vamos incluir um método que poderá ser chamado pelo Controller para solicitar que a recarga aconteça.

Os mesmos princípios de bloqueamento de thread se aplicam ao iOS e, embora ele não proíba que operações de rede aconteçam na UI Thread, felizmente a classe URLSession gerencia automaticamente para que essas operações ocorram em segundo plano em uma thread separada. No arquivo CountryList.swift inclua o método abaixo:

func refreshListFromGeonamesService(_ completionHandler: ((_ success: Bool) -> ())?) {
    // 1.
    let downloadDataTask = URLSession.shared.dataTask(with: CountryList.COUNTRY_INFO_URL, completionHandler:
        { (data, response, error) -> Void in
            // 2.
            if let e = error {
                print("Error trying to download data: \(e)")

                // 3.
                if let c = completionHandler {
                    DispatchQueue.main.async {
                        c(false)
                    }
                }

                return
            }

            // 4.
            self.parseGeonamesCountryInfoJsonData(data!)
            if let c = completionHandler {
                DispatchQueue.main.async {
                    c(true)
                }
            }
    })

    // 5.
    downloadDataTask.resume()
}

Esse método concentra toda a lógica básica para acesso a um serviço web usando HTTP. Para facilitar sua compreensão, quebramos a explicação em partes numeradas pelos comentários:

  1. O primeiro passo é criar um objeto URLSessionDataTask, retornado pelo método dataTask do URLSession. Esse método recebe 2 parâmetros:
    1. url: um objeto do tipo URL contendo o endereço do recurso HTTP no qual será feito o request.
    2. completionHandler: uma closure declarando a função que fará o tratamento do response, após a o URLSession executar o request. Essa função, por sua vez, recebe 3 parâmetros:
      1. data: um objeto do tipo Data contendo o corpo do response. Nesse caso em específico ele conterá o documento JSON representando os dados dos países.
      2. response: um objeto do tipo URLResponse, onde podemos acessar informações como o código de status e os cabeçalhos do response.
      3. error: um objeto do tipo Error que devemos verificar para saber se ocorreu algum problema no processamento do request. Note que ele é um optional, portanto se nenhum erro tiver acontecido seu valor será nil.
  2. O primeiro passo é verificar se o parâmetro error não é nulo.
  3. Para o caso de um erro ter acontecido, notificamos o callback de que a operação foi concluída sem sucesso. Note que essa chamada é feita na thread principal.
  4. Com o request tendo sido executado com sucesso, chamamos o método parseGeonamesCountryInfoJsonData(), responsável por processar os dados retornados pelo serviço do GeoNames, em formato JSON, e inseri-los no Array da propriedade countries do objeto, contendo a listagem de países. O Xcode deve reportar um erro nessa linha pois ainda vamos implementar esse método a seguir.
  5. Ao chamar o método resume() no objeto URLSessionDataTask, a operação entra na fila de execução do URLSession, sendo executado em background de maneira assíncrona.

Ainda no arquivo CountryList.swift, inclua a implementação do método parseGeonamesCountryInfoJsonData conforme abaixo:

private func parseGeonamesCountryInfoJsonData(_ data: Data) {
     // 1.
    let json = JSON(data: data)
    let items = json["geonames"].arrayValue

     // 2. 
    self.countries.removeAll(keepingCapacity: false)

     // 3.
    for item in items {
        let countryName = item["countryName"].stringValue
        let countryCode = item["countryCode"].stringValue
        let capital = item["capital"].stringValue
        let continent = item["continent"].stringValue
        let population = item["population"].intValue
        let area = item["areaInSqKm"].floatValue

    // 4.
        self.countries.append(Country(code: countryCode, name: countryName, capital: capital, continent: continent, population: population, area: area))
    }
}

Esse método recebe um parâmetro do tipo Data, contendo o documento JSON com as informações dos países. Vamos entender cada passo desse código:

  1. Criamos uma variável json do tipo JSON da biblioteca SwityJSON. Na sequência extraímos o array de objetos representando pela propriedade geonames na variável items.
  2. Removemos todos os elementos da propriedade countries, pois vamos processá-los novamente a partir dos dados recebidos.
  3. Passamos por cada um dos elementos do array, extraindo suas propriedades em variáveis.
  4. Criamos um objeto do tipo Country a partir dos dados extraídos e adicionamos à lista da propriedade countries.

O último ajuste que temos de fazer no modelo é na classe Country que representa os dados de um país. Vamos incluir uma propriedade nela para facilitar obter a URL da bandeira que representa aquele país, conforme explicamos na seção do serviço GeoNames.

Trata-se de uma propriedade computada, ou seja, que tem apenas um getter que processa o valor a ser retornado sempre a propriedade é acessada. Abra o arquivo Country.swift e no final da classe inclua o seguinte código:

var flagUrl: URL {
    return URL(string: "http://www.geonames.org/flags/x/\(self.code.lowercased()).gif")!
}

A classe URL do Swift é equivalente à classe NSURL do Cocoa Touch, presente no Foundation, e representa o caminho para acessar um recurso remoto. Podemos passar uma String para inicializa-la. Note que utilizamos a propriedade code mas, que precisamos chamar o método lowercased() para obter sua versão em letras minúsculas, já que o serviço GeoNames é case-sensitive.

E isso é tudo que precisamos fazer para ajustar o modelo!

Solicitando que as informações de países sejam baixadas

O próximo passo é conectar a interface do App às modificações que promovemos no modelo. Na barra de navegação da tela principal, incluímos um botão do tipo Refresh (cujo o ícone é uma seta circular representando essa operação). O projeto inicial já inclui o Action Method desse botão, representando pelo método refreshTapped:. No arquivo CountriesViewController.swift complete seu código conforme abaixo:

@IBAction func refreshTapped(_ sender: UIBarButtonItem) {
    let p = MBProgressHUD.showAdded(to: self.navigationController!.view, animated: true)
    p.label.text = "Loading Countries"

    self.countryList.refreshListFromGeonamesService { (success) -> () in
        MBProgressHUD.hide(for: self.navigationController!.view, animated: true)
        if success {
            // Reload data on success
            self.tableView.reloadData()
        } else {
            // Display message
            let alert = UIAlertController(title: "Countries", message: "Unable to load country data from the server.", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
            self.present(alert, animated: true, completion: nil)
        }
    }
}

Esse código utiliza o componente MBProgressHUD para exibir uma animação de progresso, enquanto a operação de baixar os países e carregá-los para o modelo é processada. Certifique-se de que o módulo desse componente foi importado, incluindo a diretiva no início do arquivo CountriesViewController.swift:

import MBProgressHUD

Feito isso, nosso app já está pronto para ser testado! Compile e execute-o em um simulador ou em um dispositivo de testes. Clique no botão Refresh na barra de navegação e verifique se a listagem dos países foi carregada com sucesso.

Exibindo a bandeira dos países na listagem

Por hora nosso App apresenta a imagem de bandeira não identificada para todos os países na listagem. Isso porque precisamos configurar a célula da listagem para que a imagem da bandeira seja carregada. Felizmente essa tarefa fica bastante simples com o uso do componente SDImageView. Ele inclui uma série de extensões à classe UIImageView, permitindo passar uma URL com a imagem que desejamos que ele exiba.

O componente faz o download das imagens de maneira assíncrona e quando finalizado exibe automaticamente no Image View em questão. Ele também armazena uma cópia dessas imagens em um cache temporário, então se o usuário sair e voltar do App ele vai primeiro tentar usar uma versão local antes de disparar um novo download. Por último, mas não menos importante, é possível definir uma imagem placeholder, que é exibida por padrão enquanto a imagem desejada ainda não está disponível.

E para aproveitar todas as características desse componente, tudo o que precisamos fazer é incluir uma única linha de código no arquivo CountryViewCell.Swift. Localize a propriedade a country e dentro do if inclua a chamada ao método sd_setImage(), onde passamos como primeiro parâmetro a URL da imagem da bandeira e, em segundo, a imagem que queremos usar como placeholder a “bandeira desconhecida”:

var country: Country? {
    didSet {
        if let country = country {
            nameLabel.text = country.name
            capitalLabel.text = "Capital: \(country.capital)"
            flagImageView.sd_setImage(with: country.flagUrl, placeholderImage: UIImage(named: "UnknownFlag")!)
        }
    }
}

Por último, verifique se o módulo WebImage foi importado ainda no mesmo arquivo, pois é lá que se encontra o método adicional que usamos acima.

import WebImage

Isso é tudo que precisamos fazer em nosso código para que as bandeiras comecem a ser carregadas na listagem! Compile; execute o app e verifique se as imagens estão sendo corretamente carregadas:

Exibindo a bandeira dos países na tela de detalhes

Nosso último exercício dessa prática será a inclusão da bandeira do país na tela de detalhes. Para demostrar uma possibilidade, vamos apresentar a bandeira na barra de navegação da tela, no lado direito.

O componente UINavigationItem do iOS define a propriedade rightBarButtonItem, que nos permite atribuir um objeto do tipo UIBarButtonItem. Ao contrário do que o nome sugere, podemos incluir qualquer objeto derivado de UIView dentro desses botões da barra de navegação e de ferramentas (a que desenhamos na parte de baixo da tela). Isso inclui o UIImageView. Inclua o código abaixo no final do método viewDidLoad do arquivo CountryDetailsViewController.swift, que cria programaticamente um Image View e usa esse truque para exibi-lo na barra de navegação:

// Load flag image aside
let unknownImage = UIImage(named: "UnknownFlag")
let flagImage = UIImageView(image: unknownImage)
flagImage.contentMode = UIViewContentMode.scaleAspectFill
flagImage.frame = CGRect(x: 0, y: 0, width: 40, height: 20)
flagImage.sd_setImage(with: self.country.flagUrl as URL, placeholderImage: unknownImage)

let customBarItem = UIBarButtonItem(customView: flagImage)
self.navigationItem.rightBarButtonItem = customBarItem

Isso é tudo que precisamos fazer para configurar a exibição da bandeira. Execute o App novamente e teste suas alterações.

Você conectou seu App iOS! 👏🏻😊

results matching ""

    No results matching ""