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"