Alamofire

Nous allons travailler sur un projet qui indiquera des informations (emplacement, ouverture, …) sur les toilettes publiques de la ville de Paris. (welcom tourists …soon...)

Nous utiliserons le jeu de données de data.gouv.fr/fr

Testons la réponse JSON sur Postman , avec l’url fournie dans la documentation :

https://www.data.gouv.fr/fr/datasets/r/9cf8fab8-997c-4814-9600-8c17bc3de7e0

1- Création du projet

Nous créons un projet xcode et organisons les fichiers suivant le design pattern MVC

Puis créer un repository sur gitHub

Sur notre "terminal", se positionner dans le répertoire du projet, et suivre les instructions du quick setup de github

$git init
$git add .
$git commit –m "creation projet"
$git remote add origin https://github.com/…....etc......... 
$git push –u origin master

Sur github ajouter le ficher readme

Retour sur le "terminal" pour récupérer le "readme"

$git pull origin master

Vérifier la récupération du fichier readme dans le dossier local avec la commande : $ls

2- Installation pod Alamofire

Récupérer le pod à installer dans la documentation Alamofire sur cocoapods.org

Sur le "terminal", initialiser pod et ouvrir le podfile avec xcode

$pod init
$ open –a xcode podfile

dans le fichier podfile, ajouter le pod fournit par la documentation Alamofire

pod 'Alamofire', '~> 5.2'

enregistrer le podfile et quitter en fermant xcode

Retour sur le "terminal", et installer le nouveau pod

$pod install

vérifier que "pod installation complete !" s’affiche bien dans le terminal.

3- Relancer le PROJET

Allez dans le "Finder"/dossier projet, ouvrez le projet avec extension .xcworkspace

Vous retrouvez dans le dossier pods, le pod Alamofire.

Vous pouvez committer votre projet.

4- Allons chercher les données

Pour gérer la requête des données, nous ajoutons un fichier "Service.swift", dans le groupe Model, ou dans un autre groupe Services

Importer Alamofire

Import Alamofire

créer une propriéte pour l'url de le requête

fileprivate var baseUrl = "https://www.data.gouv.fr/fr/datasets/r/9cf8fab8-997c-4814-9600-8c17bc3de7e0"

et une fonction getDataSet().

Nous ouvrons la session avec la référence AF, et utilisons des fonctions d'Alamofire :

  • pour la requête : .request(convertible: URLConvertible, method: HTTPMethod, parameters: Parameters?, encoding: ParameterEncoding, headers: HTTPHeaders?, interceptor: RequestInterceptor?, requestModifier: Session.RequestModifier?)

  • pour la réponse : .response(completionHandler: (AFDataResponse) -> Void)

completionHandler est une closure pour traiter la réponse (responseData) quand elle sera arrivée.

Pour l’instant on vérifie avec un print "nous avons la réponse"

func getDataSet() {
        AF.request(self.baseUrl, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil, requestModifier: nil)
             .response { (responseData) in
                          print("nous avons la réponse")
            }
    }

et nous testons en ajoutant dans le ViewController.swift/viewDidLoad() :

let service = Service()

service.getDataSet()

Nous obtenons dans la console "nous avons la réponse".

L'annexe à la fin de l'article indique d'autres fonction d'Alamofire

5- Le MODELE de la réponse

Avec Postman nous voyons la réponse en JSON

5 désignations sont souvent répétées :

  • "datasetid": "sanisettesparis",
  • "recordid": "…",
  • "fields": {...},
  • "geometry" : {…},
  • "record_timestamp": "... "

et encapsulées dans des accolades.

Chaque accolade représente une sanisette donc un objet.

Dans le groupe MODEL de notre projet, ajoutons un nouveau file "Toilettes.swift" , et implémentons une structure pour représenter l'objet

struct Toilette: Decodable {
       let datasetid : "sanisettesparis", 
       let recordid : "…", 
       let fields : {...}, 
       let geometry : {…}, 
       let record_timestamp : "... "
}

Parmi les 5 désignations, le champs "fields" comprend entre accolades les informations qui nous intéresse : adresse, accessibilité, statut, horaire, ....

Seule la propriété "fields" caractérisera notre objet "Toilette"

struct Toilette: Decodable {
         let fields : {...}
}

"fields" comprend plusieurs désignations entre accolade. Comme pour l'objet "Toilette", nous pouvons le modéliser en objet avec des propriétés pour le caractérisé.

Créons une structure pour le représenter, uniquement avec les informations retenues

struct Fields: Decodable {
    let statut: "Fermé"
    let arrondissement: "13"
    let adresse: "26 PLACE JEANNE D ARC"
    let geo_point_2d : [48.8297799503,2.36896816348]
}

La plupart des propriétés sont de type String. Les coordonnées GPS, "geo_point_2d", sont de type Double, dans un tableau.

Remplaçons les valeurs des propriétés par leur type.

Pour représenter l'encapsulage des accolades du fichier JSON, les deux structure sont associées par leur type.

Pour l'objet "Toilette", la propriété "fields" sera de type Fields

struct Toilette: Decodable {
    let fields: Fields

    enum CodingKeys: String, CodingKey {
        case fields = "fields"
    }
}

struct Fields: Decodable {
    let statut: String?
    let arrondissement: String?
    let adresse: String?
    let geo_point_2d : [Double]?

    enum CodingKeys: String, CodingKey {
        case statut = "statut"
        case arrondissement = "arrondissement"
        case adresse = "adresse"
        case geo_point_2d = "geo_point_2d"
    }
}

Les propriétés sont en optionnelles pour éviter l'erreur si un champs est vide. Nous ajoutons une enum pour que chaque propriété corresponde bien au champ du fichier JSON.

6- DECODER la réponse

Revenons un instant au fichier JSON. Le fichier de toutes les sanisettes commence et se termine par des crochets, donc un tableau. La réponse décodée sera donc placée dans un tableau [Toilette].

Dans la fonction getDataSet(), à la place du "print", nous décodons la réponse avec JSONDecoder().

Testons avec un print juste après le décodage.

func getDataSet() {
        AF.request(self.baseUrl, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil, requestModifier: nil)
         .response { (responseData) in
            guard let dataIn = responseData.data else { return }
                    do {
                        let toilettes = try JSONDecoder().decode([Toilette].self, from: dataIn)
                        print("Toilettes == \(toilettes)")
                    } catch {
                        print("error decodage == \(error))
                    }
            }

Nous lançons l’application et obtenons dans la console un tableau avec toutes les toilettes.

7- AFFICHAGE dans une tableView

Dans un premier temps, nous affichons juste l'adresse, l'arrondissement, et le statut des toilettes, dans une liste.

Nous passons en commentaires les appels de "Service" dans le "viewDidLoad" du "ViewController.swift".

Dans le Storyboard, nous ajoutons un TableViewController, et un bouton dans le ViewController.

Relier le bouton vers la table, et dans le "ViewController" avec un @IBAction. Donner un identifier à la cellule.

Créer un CocoaTouch file pour le TableViewController, "ToilettesTVController.swift" Ajouter dans ce fichier un @IBOutlet pour la table, un tableau de Toilettes, et compléter les fonctions du TableViewDataSource protocole déjà implémenter.

Pensez à relier @IBOutlet de la tableView. Vérifier le style (subtitle) de la cellule.

class ToilettesTVController: UITableViewController {

    @IBOutlet weak var toilettesTable: UITableView!

    var toilettes = [Toilette]()

    override func viewDidLoad() {
        super.viewDidLoad()
        }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return toilettes.count
    }

 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ToiletteCell", for: indexPath)
        let toilette = toilettes[indexPath.row].fields
        cell.textLabel?.text = (toilette.adresse ?? "") + " " + (toilette.arrondissement ?? "")
        cell.detailTextLabel?.text = toilette.statut ?? ""

        return cell
    }
}

8- PASSER les data avec un CALLBACK

Nous allons utiliser un CallBack pour passer les données du Modèle "Service" au Controller "ToilettesTVController".

Dans Service.swift, nous créons un typealias pour le callback, qui sera une closure de type fonction.

Ce callback enverra le tableau de Toilette quand la requête sera réussie. Donc deux paramètres pour cette fonction, le statut pour la réussite ou l'échec de la requête, et le tableau de toilettes

typealias toilettesCallBack = (_ statut: Bool, _ toilettes: [Toilette]?) -> Void

Passer en paramètre de la fonction getDataSet() le callback de type toilettesCallBack, et remplacer les "print" de cette fonction par des callback

callback(true, toilettes) en cas de succès, autrement,

callback(false, nil)

func getDataSet(callback: @escaping toilettesCallBack) {

        AF.request(self.baseUrl, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil, requestModifier: nil).response { (responseData) in
            guard let dataIn = responseData.data else {
                callback(false, nil)
                return }
                    do {
                        let toilettes = try JSONDecoder().decode([Toilette].self, from: dataIn)
                        print("Toilettes == \(toilettes)")
                        callback(true, toilettes)
                    } catch {
                        callback(false, nil)
                    }
            }
    }

@escaping garde en mémoire le résultat de la closure et la closure elle même pour pouvoir être réutilisée.

9- Récupérer le CALLBACK

Dans ToilettesTVController.swift, nous implémentons une nouvelle fonction qui lance la requête, récupère le callBack, et affecte le résultat au tableau toilettes.

func receiveData() {
            let service = Service()
            service.getDataSet { [weak self] (statut, toilettes) in
                guard let self = self else { return }
                if statut {
                    guard let toilettes = toilettes else { return }
                    self.toilettes = toilettes
                    self.toilettesTable.reloadData()
                } else {
                    self.toilettes = []
                } 
            }

Appeler cette fonction dans le viewDidLoad()

Au lancement de l'application, appuyer sur le bouton liste pour voir la liste des toilettes...

commit : 39379a2a067802c37353c1df7ac34d8a7e897060

ANNEXE

Nous pouvons obtenir dans la console le fichier JSON avec la méthode .reponseJSON

AF.request(baseUrl)
     .responseJSON { (response) in
                print(response)
            }

la méthode .response(completionHandler: (AFDataResponse) -> Void) que nous utilisons plus haut, associée à la méthode request(convertible: URLConvertible), nous fournira les informations générales du header, la taille de la réponse etc.. avec un debugPrint(response), et juste le statut de la réponse et sa taille avec un print(response)

AF.request(baseUrl)
            .response { (response) in
               debugPrint(response)
            }

NB : Si votre adresse URL est en HTTP et non en HTTPS, alors erreur dans la console ("App Transport Security has blocked).

Vous pouvez corriger ce statut, en cliquant sur le fichier "info.plist" pour le selectionner, ouvrir son menu (ctrl clic), choisir "open as" puis "Source code", et ajouter à la fin du fichier, après /array et avant /dict /plist

</array>

(début partie à ajouter)

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

(fin partie à ajouter)   

</dict>
</plist>

Ou plus simple, dans le info.plist, ajouter la key "App Transport Security Settings", cliquer sur le + pour ajouter "Allow Arbitrary Loads" que vous renseigné à "yes"

Capture d’écran 2021-04-28 à 16.54.41.png