Révision du 2015.11.16 – conversion vers Xcode 6.4
Dans le tutoriel TIMFlix, nous avons construit une application qui affichait une liste de vidéos à l’intérieur d’un UITableView.
Les informations des vidéos, ainsi que l’image des affiches, étaient renseignées localement grace à un fichier de propriétés.
L’inconvénient de cette approche est évident lorsque vient le temps d’y ajouter de nouvelles vidéos.
Dans ce tutoriel, nous verrons comment utiliser une API RESTFull pour fournir des données à une application Xcode.
Vidéo du résultat final:
C’est une pratique courante que d’offrir l’accès à des données via des URLs web selon la méthode RESTful.
L’avantage de cette approche est qu’il n’est pas nécessaire d’offrir un accès direct au SGBD (système de gestion de la base de données) de l’organisation.
Par exemple, via une connexion à la BD par programmation, par utilisation d’outils comme phpMyAdmin, Oracle connect ou, des connaissances techniques du SGBD.
La technique, est d’offrir des scripts de type ‘serveur web’ de traitements et d’accès à la base de données.
Ces scripts, écrits en PHP, ASP, Python, …, peuvent alors être lancés à partir d’une simple requête HTTP soit dans un fureteur ou soit dans une application: en utilisant des fonctions ou de objets tel que curl(), NSArray(URL:) et autres.
Voir la définition de Representational State Transfer.
Pour l’interrogation des données, le script peut retourner les données dans un format indépendant du SGBD tel que;
Texte, XML, JSON, CSV et PList.
Par exemple, l’URL suivante,
http://uneApi.org/obtenir_liste_stages.php?region=mtl&format=csv
Pourrait retourner la liste des lieux de stage pour la région de Montréal en format csv:
« Ubisoft », »2″, »Hiv15″
« Gameloft », »1″, »Hiv15″
Pour programmer une API Web d’accès à une base de données il suffit d’avoir accès à un serveur Web (IIS, Apache, …), à un langage script coté serveur (php, ASP, node.JS, …) et à un SGBD (MySQL, MSSQL, Oracle, PostgreSQL, … ).
Dans le cadre d’apprentissages, une solution comme WAMP est idéale.
En pratique, voici comment offrir un API Web d’interrogation d’une table proposant des pensées du jour:
API.TIM.01 – À partir d’une table, ‘penseesdujours’:
API.TIM.02 – D’un script PHP à l’adresse ‘/unScript.php’
// Requête SQL pour obtenir les enregistrements de la table penseesdujours // Connexion à la BD mysql_connect("localhost", "user", "password") or die(mysql_error()); mysql_select_db("cours_xcode") or die(mysql_error()); // Exécuter un requête SQL $res = mysql_query("SELECT `nom_ajout` , `created_at` , `adresse_ip` , `pensee_texte`, `pensee_auteur`, `pensee_lien_image` FROM penseesdujours") or die(mysql_error()); // Parcourir les éléments du tableau de résultats while($r = mysql_fetch_assoc($res)) { $xx = array_map("utf8_encode", $r); foreach ($xx as $key => $value) { echo "clé = $key, valeur = $value\n"; } // foreach } // while encore un résultat
API.TIM.03 – L’API retournera, au client du lien, le texte suivant:
clé = nom_ajout, valeur = Alain
clé = created_at, valeur = 2014-11-01 06:11:47
clé = adresse_ip, valeur = 24.200.185.163
clé = pensee_texte, valeur = On ne reçoit pas la sagesse, il faut la découvrir soi-même, après un trajet que personne ne peut faire pour nous, ne peut nous épargner.
clé = pensee_auteur, valeur = Marcel Proust
clé = pensee_lien_image, valeur = http://upload.wikimedia.org/wikipedia/commons/thumb/7/7e/Marcel_Proust_1900.jpg/220px-Marcel_Proust_1900.jpg
clé = nom_ajout, valeur = Alain
clé = created_at, valeur = 2014-11-01 06:11:05
clé = adresse_ip, valeur = 24.200.185.163
clé = pensee_texte, valeur = Ce qui compte, chez un homme, ce n’est pas la couleur de sa peau ou la texture de sa chevelure, mais la texture et la qualité de son âme.
clé = pensee_auteur, valeur = Martin Luther King
clé = pensee_lien_image, valeur = http://seattletimes.com/art/mlk/index.jpg
lien de test
API.TIM.04 – Voici un script PHP de mise en format ‘plist’ – voir la structure d’un fichier ‘plist’ sous Xcode:
echo '<?xml version="1.0" encoding="UTF-8"?>'."\n"; echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'."\n"; echo '<plist version="1.0">'."\n"; echo "<!-- Liste générée par l'API TIM le ".date('Y-m-d h:m:s')." -->\n"; echo "<!-- (c) 2014 par Alain Boudreault -->\n"; echo "<array>\n"; while($r = mysql_fetch_assoc($res)) { $xx = array_map("utf8_encode", $r); echo "\t<dict>\n"; foreach ($xx as $key => $value) { echo "\t\t<key>".$key."</key>\n\t\t<string>".$value."</string>\n"; } // foreach echo "\t</dict>\n"; } // while echo "</array>\n</plist>";
API.TIM.05 – Produira le résultat suivant:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <!-- Liste générée par l'API TIM le 2014-11-02 09:11:55 --> <!-- (c) 2014 par Alain Boudreault --> <array> <dict> <key>nom_ajout</key> <string>Alain</string> <key>created_at</key> <string>2014-11-01 06:11:47</string> <key>adresse_ip</key> <string>24.200.185.163</string> <key>pensee_texte</key> <string>On ne reçoit pas la sagesse, il faut la découvrir soi-même, après un trajet que personne ne peut faire pour nous, ne peut nous épargner.</string> <key>pensee_auteur</key> <string>Marcel Proust</string> <key>pensee_lien_image</key> <string>http://upload.wikimedia.org/wikipedia/commons/thumb/7/7e/Marcel_Proust_1900.jpg/220px-Marcel_Proust_1900.jpg</string> </dict> </array> </plist>
lien de test
API.TIM.06 – Un script PHP de mise en format ‘JSON‘
// tableau du résultat final $resultat = array(); $resultat['info'] = array("API_TIM" => "version 2014.10.01", "type_requete" => Input::get('mode')); while($r = mysql_fetch_assoc($res)) { // Encoder les caractères accentués. $tableauAvecAccents[] = array_map("utf8_encode", $r); } // while $resultat['resultat'] = $tableauAvecAccents; // Convertir un tableau en format JSON echo json_encode($resultat);
API.TIM.07 – Produira le résultat suivant:
{ "info":{"API_TIM":"version 2014.10.01","type_requete":"rnd"}, "resultat":[ { "nom_ajout":"admin", "created_at":"2014-11-01 10:11:56", "adresse_ip":"24.200.185.163", "pensee_texte":"La qualit\u00e9 d'un homme se calcule \u00e0 sa d\u00e9mesure ; tentez, essayez, \u00e9chouez m\u00eame, ce sera votre r\u00e9ussite\u0085", "pensee_auteur":"Jacques Brel", "pensee_lien_image":"https:\/\/encrypted-tbn2.gstatic.com\/images?q=tbn:ANd9GcQqWOL6fBMrhIECWoOuY3aEdxrm6biaZTdi5Kp72TC_xG9L11GS" }, ... ] }
lien de test
Lien vers l’API TIM (utilisateur: étudiant:tim)
Exemple complet (Laravel)
<?php /* -------------------------------------------------------------- Projet: Classe: requetes.php Auteur: Alain Boudreault Date: 2015.02.10 -------------------------------------------------------------- Description: -------------------------------------------------------------- Entrée: Sortie: -------------------------------------------------------------- M-A-J: -------------------------------------------------------------- */ class Requetes_api_Controller extends Base_Controller { function __construct() { parent::__construct(); } public function action_index() { $user = is_null(Auth::user())?'':Auth::user()->username; if (Input::get('type') == 'liste_pensees') { $mode = Input::get('mode'); $quant = Input::get('quant'); Trace::ajouter($user,'API','Requête: Liste des pensées', 0); return View::make('requetes.apiweb') ->with('quant', $quant) ->with('mode', $mode) ->with('format', Input::get('format')); } // if config } }
@layout('vide') @section('content') <?php // Fichier: apiweb.blade.php // Auteur: Alain Boudreault // Requête SQL pour obtenir les enregistrements de la table penseesdujours mysql_connect("localhost", "aboudrea", "lnAvril67") or die(mysql_error()); mysql_select_db("cours_xcode") or die(mysql_error()); $rows = array(); // Résultat final $rows['info'] = array("API_TIM" => "version 2014.10.01", "type_requete" => Input::get('mode')); if (Input::get('mode') == 'all') { $res = mysql_query("SELECT `nom_ajout` , `created_at` , `adresse_ip` , `pensee_texte`, `pensee_auteur`, `pensee_lien_image` FROM penseesdujours") or die(mysql_error()); } // mode=all if (Input::get('mode') == 'rnd') { $quant = Input::get('quant'); $res = mysql_query("SELECT `nom_ajout` , `created_at` , `adresse_ip` , `pensee_texte`, `pensee_auteur`, `pensee_lien_image` FROM penseesdujours ORDER BY RAND() LIMIT $quant") or die(mysql_error()); } // mode=rnd if (Input::get('mode') == 'adulte') { $quant = Input::get('quant'); $res = mysql_query("SELECT * FROM (SELECT `nom_ajout` , `created_at` , `adresse_ip` , `pensee_texte`, `pensee_auteur`, `pensee_lien_image` FROM penseesdujours where categorie = 3) as tmp ORDER BY RAND() LIMIT $quant") or die(mysql_error()); } // mode=adulte // Lecture des pensees du jour if(!$res) { echo "Error: no results from table devices"; die(json_encode(array("error" => "no results from table"))); } // Parcourir les éléments du tableau de résultats if (Input::get('format') == 'texte') { while($r = mysql_fetch_assoc($res)) { $xx = array_map("utf8_encode", $r); foreach ($xx as $key => $value) { echo "clé = $key, valeur = $value<br />\n"; } } // while } // format=texte else { if (Input::get('format') == 'plist') { echo '<?xml version="1.0" encoding="UTF-8"?>'."\n"; echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'."\n"; echo '<plist version="1.0">'."\n"; echo "<array>\n"; while($r = mysql_fetch_assoc($res)) { $xx = array_map("utf8_encode", $r); echo "\t<dict>\n"; foreach ($xx as $key => $value) { echo "\t\t<key>".$key."</key>\n\t\t<string>".$value."</string>\n"; } echo "\t</dict>\n"; } echo "</array>\n</plist>"; } else // format json { while($r = mysql_fetch_assoc($res)) { $sous_ensemble[] = array_map("utf8_encode", $r); } $rows['resultat'] = $sous_ensemble; echo json_encode($rows); } } ?> @endsection
Dans un nouveau projet,
Action tableau.01 – Créer un Array<Dictionary<String, String>> à partir d’une URL via NSArray():
func testerTableau01() { // Création d'un Array<Dictionary<String, String>> à partir d'une URL let urlAPI = "/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=all&format=plist" let tableauDonnees = NSArray(contentsOfURL: NSURL(string: urlAPI)!) as! Array<Dictionary<String, String>> if tableauDonnees.count == 0 { println("Pas de tableau!") } else { println(tableauDonnees) } // if tableauDonnees } // testerTableau01() testerTableau01()
Action tableau.02 – Utiliser des types ‘Foundation (NS)’, pour accéder aux éléments du tableau:
Note: À ajouter dans la section « else {} ».
// Note: Obtenir et afficher un des éléments du tableau. let w = tableauDonnees[4] println(w) let unePensee = w["pensee_texte"]! println(unePensee) for x in tableauDonnees { var texte = "----------------------------------\n" let auteur = x["pensee_auteur"]! let pensee = x["pensee_texte"]! texte += "L'auteur \(auteur) a dit:\n\(pensee)" println(texte) } // for x in tableau
Action tableau.03 – Créer une optionnelle de type Array à partir d’une URL:
let tableauDonnees = NSArray(contentsOfURL: NSURL(string: "/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=all&format=plist")!) as Array? if tableauDonnees == nil { println("Pas de tableau!") } else { println(tableauDonnees) } // if tableauDonnees
Action tableau.04 – Utiliser des optionnelles pour accéder aux éléments du tableau:
for x in tableauDonnees { println("\n----------------------------------") if let y = x["pensee_auteur"] as? String{ println("L'auteur \(y) a dit:\n") } if let y = x["pensee_text"] as? String{ println(y) } } // for x in tableau
[expand title= »Afficher la solution »]
[/expand]
Note: Il faut utiliser ‘?’ pour le typage d’une affectation optionnelle: as? String.
Action tableau.05 – Modifiez le code précédent pour afficher une trace lorsqu’une des clés du tableau est invalide.
Action tableau.06 – Définition d’un tableau complexe:
var tableauComplexe:Dictionary<String, AnyObject>? tableauComplexe = [ "nom":"Bob", "amis": [ ["nom":"ami01", "email":"x@y.com"], ["nom":"ami02", "email":"a@b.com"] ] , "pointage": [2,45,56] ] println(tableauComplexe)
Résultat:
Optional(["nom": Bob, "pointage": <_TtCSs23_ContiguousArrayStorage00007FB951786DA0 0x7fb951787d10>( 2, 45, 56 ) , "amis": <_TtCSs23_ContiguousArrayStorage00007FB951785B80 0x7fb951785140>( { email = "x@y.com"; nom = ami01; }, { email = "a@b.com"; nom = ami02; } ) ])
Note importante: Lorsqu’un Dictionary contient des éléments complexes de type AnyObject, les éléments doivent alors être typés vers des classes de type NSObject.
Action tableau.06.01 – Accès aux éléments d’un tableau complexe:
// Trouver les erreurs! let lesAmisDeBob = tableauComplexe["amis"] println(lesAmisDeBob)
[expand title= »Afficher la solution »]
Résultat:
Optional(<_TtCSs23_ContiguousArrayStorage00007FBC1AE13F40 0x7fbc1ae134d0>( { email = "x@y.com"; nom = ami01; }, { email = "a@b.com"; nom = ami02; } ) )
Note: Il est préférable de typer la déclaration,
car il ne sera pas possible de faire:
println(lesAmisDeBob[0])
Présentement, lesAmisDeBob est de type AnyObject.
La solution:
// Note: un Anyobject doit être typé vers une classe de type NSObject. let lesAmisDeBob = tableauComplexe!["amis"] as! Array<Dictionary<String, String>> println(lesAmisDeBob[0])
Action tableau.06.02 – Créez un tableau ‘lesAmisDeBob’ en utilisant une déclaration conditionnelle. Note: aucun déballage.
[expand title= »Afficher la solution »]
let INDICE_TABLEAU_AMIS = "amis" if let tableauDeballe = tableauComplexe { let lesAmisdeBob = tableauDeballe[INDICE_TABLEAU_AMIS] as! Array<Dictionary<String, String>> }
[/expand]
Action tableau.06.03 – Afficher la liste des amis
let INDICE_TABLEAU_AMIS = "amis" let CHAMP_NOM = "nomm" if let tableauDeballe = tableauComplexe { println(tableauDeballe[INDICE_TABLEAU_AMIS]) if let lesAmisDeBob = tableauDeballe[INDICE_TABLEAU_AMIS] as? Array<Dictionary<String, String>> { println("Liste des amis de Bob\n\n") for ami in lesAmisDeBob { if let nom = ami[CHAMP_NOM] { println(nom) } else { println("Erreur sur champ : \(CHAMP_NOM)") } } // for ami in } // if let lesAmisdeBob } // let tableauDeballe = tableauComplexe
Action tableau.06.04 – Afficher (sans utiliser des optionnelles dans le println()) le contenu du tableau des pointages.
Action API.TIM.01 – Dans un nouveau projet, ajoutons le code suivant:
// ViewController.swift import UIKit class ViewController: UIViewController { // Dans le cas d'un tableau créé à partir d'une plist obtenu via une URL, il est plus simple // de travailler avec les classes NS: NSString, NSNumber, NSArray, NSDictionary. // Nous ferons moins de déballage. // De plus, le type AnyObject doit être typé vers un NS. var tableauDonnees:NSArray? // [Dictionary<String, AnyObject>]? override func viewDidLoad() { super.viewDidLoad() let tableauDonnees = NSArray(contentsOfURL: NSURL(string: "/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=all&format=plist")!) as! Array<Dictionary<String, String>> // println(tableauDonnees) // Note: Obtenir et afficher un des éléments du tableau. let w = tableauDonnees[4] println(w) let unePensee = w["pensee_texte"]! println(unePensee) /* // Commentaire à enlever à la prochaine étape. // Afficher tous les éléments de tableauDonnees. // Note: pas de boucle si tableau est nil. for x in tableauDonnees { println("\n----------------------------------") if let y = x["pensee_auteur"]{ println("L'auteur \(y) a dit:\n") } if let y = x["pensee_texte"]{ println(y) } } // for x in tableau // ****** Commentaire à enlever à la prochaine étape. */ } // viewDidLoad() } // ViewController
Résultat obtenu:
// println(w) { "adresse_ip" = "24.200.185.163"; "created_at" = "2014-11-01 06:11:28"; "nom_ajout" = Alain; "pensee_auteur" = "Jacques Chardonne"; "pensee_lien_image" = "http://www.albin-michel.fr/multimedia/Article/Image/2000/9782226115287-X.jpg"; "pensee_texte" = "Poursuivre le bonheur, au lieu de le laisser venir, n'est-ce pas courir apr\U00e8s le reflet d'un mot ? En fait, les hommes seraient plus heureux si on leur parlait moins de bonheur !"; } // Println(unePensee) Poursuivre le bonheur, au lieu de le laisser venir, n'est-ce pas courir après le reflet d'un mot ? En fait, les hommes seraient plus heureux si on leur parlait moins de bonheur !
Action API.TIM.02 – Effaçons le commentaire de l’exemple précédent:
Résultat obtenu:
---------------------------------- L'auteur Marcel Proust a dit: On ne reçoit pas la sagesse, il faut la découvrir soi-même, après un trajet que personne ne peut faire pour nous, ne peut nous épargner. ---------------------------------- L'auteur Martin Luther King a dit: Ce qui compte, chez un homme, ce n'est pas la couleur de sa peau ou la texture de sa chevelure, mais la texture et la qualité de son âme. ...
Action API.TIM.03 – Observons la structure des données retournée par le lien suivant:
/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=rnd&quant=5
Structure des données:
{ "info":{"API_TIM":"version 2014.10.01","type_requete":"rnd"}, "resultat":[ { "nom_ajout":"admin", "created_at":"2014-11-01 10:11:56", "adresse_ip":"24.200.185.163", "pensee_texte":"La qualit\u00e9 d'un homme se calcule \u00e0 sa d\u00e9mesure ; tentez, essayez, \u00e9chouez m\u00eame, ce sera votre r\u00e9ussite\u0085", "pensee_auteur":"Jacques Brel", "pensee_lien_image":"https:\/\/encrypted-tbn2.gstatic.com\/images?q=tbn:ANd9GcQqWOL6fBMrhIECWoOuY3aEdxrm6biaZTdi5Kp72TC_xG9L11GS" }, ... ] }
Nous avons affaire ici à un dictionnaire de deux éléments:
Le champ ‘info’ est un dictionnaire.
Le champ ‘résultat’ est un tableau de dictionnaires.
Voici comment créer un NSDictionary à partir d’une telle structure:
Action API.TIM.04 – Ajoutons le code suivant:
// Voici comment créer un dictionnaire à partir d'un résultat JSON. // ATTENTION, il n'y a aucune validation dans les lignes qui suivent. // 1 - Créer une instance NSURL à partir d'un lien let adresseURL = "/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=rnd&quant=5" let url = NSURL(string: adresseURL) // 2 - Obtenir les données (en format brut 'NSData': des octets sans signature) let data = NSData(contentsOfURL:url!, options: nil, error: nil) println(data)
Résultat obtenu:
<7b22696e 666f223a 7b224150 495f5449 4d223a22 76657273 696f6e20 32303134 ... 5c2f4c61 66657272 69657265 2d44616e 792d7765 622e6a70 67227d5d 7d0d0a>
Tester avec : http://finance.yahoo.com/webservice/v1/symbols/allcurrencies/quote?format=json
et: /xcode/wp-content/uploads/2013/10/tomato1.png
Action API.TIM.05 – Ajoutons le code suivant:
// 3 - Convertir les données, du format JSON, vers un Dictionary <String, AnyObject> let resultat = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers, error: nil) as! Dictionary<String, AnyObject> println(resultat)
Résultat obtenu:
{ info = { "API_TIM" = "version 2014.10.01"; "type_requete" = rnd; }; resultat = ( { "adresse_ip" = "24.200.185.163"; "created_at" = "2014-11-01 06:11:38"; "nom_ajout" = Alain; "pensee_auteur" = "F\U00e9lix Leclerc"; "pensee_lien_image" = "https://lh3.googleusercontent.com/Slux6I1NENJmWrdQvLEGQkHgZOuw9G8Fln1ZGR-qTzEzHnoYaO4wHo5cNn7EiY0Q3fwBbAuq-Vszcqc8b-oxzQHgDRFsdkV7V75stzUNP3dfUUQvjBsULifh2g"; "pensee_texte" = "Le mariage, c'est deux billets d'avion aller seulement, vers une \U00eele inconnue. On en revient \U00e0 la nage ou jamais."; }, { "adresse_ip" = "24.200.185.163"; "created_at" = "2014-11-01 10:11:56"; "nom_ajout" = admin; "pensee_auteur" = "Jacques Brel"; "pensee_lien_image" = "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcQqWOL6fBMrhIECWoOuY3aEdxrm6biaZTdi5Kp72TC_xG9L11GS"; "pensee_texte" = "La qualit\U00e9 d'un homme se calcule \U00e0 sa d\U00e9mesure ; tentez, essayez, \U00e9chouez m\U00eame, ce sera votre r\U00e9ussite\U0085"; }, { "adresse_ip" = "24.200.185.163"; "created_at" = "2014-11-01 05:11:18"; "nom_ajout" = Alain; "pensee_auteur" = Him; "pensee_lien_image" = "http://cdn1.yourstory.com/wp-content/uploads/2014/04/Image-1.jpg"; "pensee_texte" = "It's the story of a guy..."; }, { "adresse_ip" = "24.200.185.163"; "created_at" = "2014-11-01 06:11:53"; "nom_ajout" = Alain; "pensee_auteur" = "Dany Laferri\U00e8re"; "pensee_lien_image" = "http://ifoa.org/ifoa/uploads/2011/07/Laferriere-Dany-web.jpg"; "pensee_texte" = "On ne meurt pas tant qu\U0092on bouge. Mais ceux qui n\U0092ont jamais franchi la barri\U00e8re de leur village attendent le retour du voyageur pour estimer si cela valait la peine de partir."; }, { "adresse_ip" = "24.200.185.163"; "created_at" = "2014-11-01 06:11:28"; "nom_ajout" = Alain; "pensee_auteur" = "Jacques Chardonne"; "pensee_lien_image" = "http://www.albin-michel.fr/multimedia/Article/Image/2000/9782226115287-X.jpg"; "pensee_texte" = "Poursuivre le bonheur, au lieu de le laisser venir, n'est-ce pas courir apr\U00e8s le reflet d'un mot ? En fait, les hommes seraient plus heureux si on leur parlait moins de bonheur !"; } ); }
Voici un exemple d’accès au dictionnaire obtenu:
Note: Dans le but de simplifier l’apprentissage, l’API TIM ne retourne que des contenus de type chaine.
Action API.TIM.06 – Ajoutons le code suivant:
// Obtenir l'élément à la clé "resultat": let lesPensees = resultat["resultat"] as! Array<Dictionary<String, String>> println(lesPensees) // Obtenir un élément du tableau lesPensees: let unePensee = lesPensees[3] println(unePensee) // Obtenir un des champs d'un élément du tableau lesPensees: let textePensee = lesPensees[3]["pensee_texte"]! println(textePensee)
Avec une syntaxe sans passer par des structures intermédiaires . Par exemple, let lesPensees = (resultat[« resultat »]) as [Dictionary<String, String>]
Action API.TIM.07 – Testons le code suivant:
let unAuteur = (((((resultat["resultat"]) as! Array)[3]) as Dictionary)["pensee_auteur"])! as String println(unAuteur)
–> Voir ‘Simplifier l’utilisation de JSON‘ au bas du document
Voici un exemple qui applique des principes de base de validation:
Action API.TIM.08 – Validons l’URL
// // Obtenir des éléments du tableau avec validation des nils: // var penseeValidee = Dictionary<String, String>() var erreur:NSError? let adresseURL = "htt://tim.cstj.qc.ca/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=rnd&quant=5" let url = NSURL(string: adresseURL) if let data = NSData(contentsOfURL:url!, options: nil, error: &erreur){ // Ici, nous avons obtenu des données vie une URL penseeValidee["donneesViaURL"] = "Réussit" //if let resultat = NSJSONSerialization.JSONObjectWithData(data } else // Erreur: URL invalide { penseeValidee["donneesViaURL"] = "Erreur de lecture des données: \(erreur)" } // else = Erreur: URL println(penseeValidee)
Action API.TIM.09 – Testons (il y aura une erreur)
// [donneesViaURL: Erreur de lecture des données: Optional(Error Domain=NSCocoaErrorDomain Code=256 "The operation couldn’t be completed. (Cocoa error 256.)" UserInfo=0x7f8d2da15bf0 {NSURL=htt://tim.cstj.qc.ca/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=rnd&quant=5})]
Action API.TIM.10 – Corrigeons l’URL (le ‘p’ suivant ‘htt’)
let adresseURL = "htt://tim.cstj.qc.ca/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=rnd&quant=5"
Résultat:
// [donneesViaURL: Réussit]
Action API.TIM.11 – Validons la conversion JSON:
var penseeValidee = Dictionary<String, String>() var erreur:NSError? let adresseURL = "/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=rnd&quant=5" let url = NSURL(string: adresseURL) if let data = NSData(contentsOfURL:url!, options: nil, error: &erreur){ // Ici, nous avons obtenu des données vie une URL penseeValidee["donneesViaURL"] = "Réussit" if let resultat = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &erreur) as? Dictionary<String, AnyObject> { // Ici, nous avons des données JSON valides. penseeValidee["conversionJSON"] = "Réussit" println("resultat: \n" + resultat.description) } else // Erreur de conversion JSON { penseeValidee["conversionJSON"] = "Erreur de conversion JSON: \(erreur)" } // if let resultat } else // Erreur: URL invalide { penseeValidee["donneesViaURL"] = "Erreur de lecture des données: \(erreur)" } // else = Erreur: URL println(penseeValidee)
Résultat:
// [donneesViaURL: Réussit, conversionJSON: Réussit]
Action API.TIM.12 – Programmons une la validation complète:
override func viewDidLoad() { super.viewDidLoad() var penseeValidee = Dictionary<String, String>() var erreur:NSError? let adresseURL = "/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=rnd&quant=5" let url = NSURL(string: adresseURL) let penseeNumero = 3 if let data = NSData(contentsOfURL:url!, options: nil, error: &erreur){ // Ici, nous avons obtenu des données vie une URL penseeValidee["donneesViaURL"] = "Réussit" if let resultat = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &erreur) as? Dictionary<String, AnyObject> { // Ici, nous avons des données JSON valides. penseeValidee["conversionJSON"] = "Réussit" // Obtenir une pensee de 'resultat' if let lesPensees = (resultat["resultat"]) as? Array<Dictionary<String, String>> { penseeValidee["CreationLesPensees"] = "Création du tableau des pensées réussit." // Ici, il y a un tableau à la clé 'resultat' // Donc, obtenir une pensée et son auteur let champ_pensee_auteur = "pensee_auteur" let champ_pensee_texte = "pensee_texte" if let pensee_auteur = lesPensees[penseeNumero][champ_pensee_auteur] { penseeValidee["pensee_auteur"] = pensee_auteur } else { penseeValidee["pensee_auteur"] = "Erreur champ: '\(champ_pensee_auteur)'\n" } if let pensee_texte = lesPensees[penseeNumero][champ_pensee_texte] { penseeValidee["pensee_texte"] = pensee_texte } else { penseeValidee["pensee_texte"] = "Erreur champ: '\(champ_pensee_texte)'\n" } } else // erreur d'accès avec le clé 'résultat' { penseeValidee["CreationLesPensees"] = "Erreur: Impossible de créer le tableau des pensées à partir de la clé 'resultat'." } // if let lesPensees } else // Erreur de conversion JSON { penseeValidee["conversionJSON"] = "Erreur de conversion JSON: \(erreur)" } // if let resultat } else // Erreur: URL invalide { penseeValidee["donneesViaURL"] = "Erreur de lecture des données: \(erreur)" } // if let data println(penseeValidee) } // viewDidLoad()
Voici un exemple d’une classe de lecture d’une source URL:JSON.
// // DonneesDuneURL.swift // tester API-TIM partie 2 // // Created by Alain on 15-10-16. // Copyright (c) 2015 Production sur support. All rights reserved. // import Foundation class DonneesDuneURL { var resultat:Dictionary<String, AnyObject>? var erreurLien:Bool = true var erreurConversion:Bool = true var error:NSError? // requit pour JSONSerialization init(adresseURL:String) { let url = NSURL(string: adresseURL) // Obtenir les données en format brut if let data = NSData(contentsOfURL:url!, options: nil, error: nil) { println("L'URL a retourné des données!") erreurLien = false // Convertir les données brutes en format JSON et les placer // dans resultat resultat = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &error) as? Dictionary // Vérifier si la conversion a fonctionné if let err = error { // Pour arriver ici, il faut que error != nil, donc de type NSError println("Pour une raison étrange, la requête a retourné une structure JSON mal formée...: \(error)"); } else { // error est 'nil' donc il n'y a pas d'erreur! println("Conversion JSON réussie"); erreurConversion = false } // if let err } else // if let data { println("Erreur: l'URL n'a pas retourné de données!") } // if let data } // init() func descriptionErreurs()->String{ return "erreurLien: \(erreurLien), erreurConversion: \(erreurConversion)" } } // class DonneesDuneURL
Note: Il y des traces dans la classe pour des besoins d’apprentissage.
Avec un exemple d’utilisation:
override func viewDidLoad() { super.viewDidLoad() let donneesAPartirDuneURL = DonneesDuneURL(adresseURL: "htt://tim.cstj.qc.ca/cours/xcode/api_tim/public/index.php/requetes_api/?type=liste_pensees&mode=rnd&quant=5") if let penseesValidees = donneesAPartirDuneURL.resultat?["resultat"] as? Array<Dictionary<String, String>> { for data in penseesValidees{ println("\n----------------------------------") // println("x: \(data)\n") if let y = data["pensee_auteur"]{ println("L'auteur \(y) a dit:\n") } if let y = data["pensee_texte"]{ println(y) } } // for data in penseesValidees } else { // erreur avec les données println(donneesAPartirDuneURL.descriptionErreurs()) } // if let penseesValidees = donneesAPartirDuneURL } // viewDidLoad()
Ce qui donne comme résultat:
---------------------------------- L'auteur Félix Leclerc a dit: Le mariage, c'est deux billets d'avion aller seulement, vers une île inconnue. On en revient à la nage ou jamais. ---------------------------------- L'auteur Bouddha a dit: Il n'existe rien de constant si ce n'est le changement. ---------------------------------- L'auteur Jacques Chardonne a dit: ...
Passons maintenant à la présentation de l’API du site ‘rottentomatoes.com’
Le site ‘RottenTomatoes’ propose une API d’accès aux données qu’il compile. Ces données nous renseignent sur des films, les critiques, les acteurs, les personnages, …
Pour interroger l’api de rottentomatoes, il suffit de suivre l’URL suivante:
http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=VOTRECLE&q=MODELE_DE_RECHERCHE&page_limit=NB_PAGES
Comme par exemple, http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=sn896rbfs3vrkcgdmndkb2&q=Star&page_limit=10
Note: La clé de l’exemple est invalide. Vous devez la remplacer par celle que vous avez obtenue de rottentomatoes. La limite de pages fixée par l’api est de 50 pages.
La requête précédente devrait nous retourner une structure JSON contenant une liste d’au plus 10 films dont la chaîne ‘Star’ apparaît dans le titre du film.
Pour obtenir, à partir du réseau Internet, des données en format JSON et créer un dictionnaire, il suffira d’utiliser la technique présentée plus haut:
NSData.dataWithContentsOfURL et
NSJSONSerialization.JSONObjectWithData
Action 1.1 – Ouvrons le projet Projet – RottenTomatoes version swift – depart et ajoutons le code suivant à la méthode ‘rechercherFilms’ de la classe contrôleur de la scène principale.
Note: Il faut inscrire votre clé programmeur dans la classe ‘cle.swift’
// ************************************************************************************************* func rechercherFilms(film:String) -> Bool { println("rechercherFilms: \(film)") progression.startAnimating() let adresseURL = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=\(CLE_API)&q=\(film)&page_limit=\(NB_FILMS)" let url = NSURL(string: adresseURL) // Obtenir les données en format brut let data = NSData(contentsOfURL:url!, options: nil, error: nil) // Si data est vide alors quitter la méthode if let resultat = data { println("L'URL a retourné des données!") } else { println("Erreur: l'URL n'a pas retourné de données!") return false } // Convertir les données brutes en format JSON et les placer // dans tableauDesFilms var error:NSError? self.tableauDesFilms = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers, error: &error) as! Dictionary<String, AnyObject> // Vérifier si la conversion a fonctionné if let err = error { // Pour arriver ici, il faut que error != nil, donc de type NSError println("Pour une raison étrange, la requête chez 'rottentomatoes' a retournée une structure JSON mal formée...: \(error)"); return false } else { // error est 'nil' donc il n'y a pas d'erreur! println("Conversion JSON réussie"); } println("tableauDesFilms = \(self.tableauDesFilms)") progression.stopAnimating() return true } // rechercherFilms
Action 1.2 – Testons l’application. Remarquez que la méthode ‘rechercherFilm’ est appelée dans ‘viewDidLoad’.
Nous devrions obtenir le résultat suivant dans la zone ‘debug » d’Xcode:
tableauDesFilms = { "link_template" = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q={search-term}&page_limit={results-per-page}&page={page-number}"; links = { next = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q=star&page_limit=1&page=2"; self = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?q=star&page_limit=1&page=1"; }; movies = ( // <-- tableau de films { // <-- dictionnaire de données du premier film de la liste "abridged_cast" = ( { characters = ( "Anakin Skywalker/Darth Vader" ); id = 162652153; name = "Hayden Christensen"; }, { characters = ( "Obi-Wan Kenobi" ); id = 162652152; name = "Ewan McGregor"; }, { characters = ( "R2-D2" ); id = 418638213; name = "Kenny Baker"; }, { characters = ( "Ruwee Naberrie" ); id = 548155708; name = "Graeme Blundell"; }, { characters = ( "Captain Colton" ); id = 358317901; name = "Jeremy Bulloch"; } ); "alternate_ids" = { imdb = 0121766; }; "critics_consensus" = "This sixth and final installment of George Lucas' epic space opera will please die-hard fanatics and non-believers alike -- largely due to awesome digital effects and the sheer power of the mythology."; id = 9; links = { alternate = "http://www.rottentomatoes.com/m/star_wars_episode_iii_revenge_of_the_sith_3d/"; cast = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/cast.json"; clips = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/clips.json"; reviews = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/reviews.json"; self = "http://api.rottentomatoes.com/api/public/v1.0/movies/9.json"; similar = "http://api.rottentomatoes.com/api/public/v1.0/movies/9/similar.json"; }; "mpaa_rating" = "PG-13"; posters = { detailed = "http://content8.flixster.com/movie/10/94/47/10944718_det.jpg"; original = "http://content8.flixster.com/movie/10/94/47/10944718_ori.jpg"; profile = "http://content8.flixster.com/movie/10/94/47/10944718_pro.jpg"; thumbnail = "http://content8.flixster.com/movie/10/94/47/10944718_mob.jpg"; }; ratings = { "audience_rating" = Upright; "audience_score" = 65; "critics_rating" = "Certified Fresh"; "critics_score" = 80; }; "release_dates" = { dvd = "2005-11-01"; }; runtime = 140; synopsis = ""; title = "Star Wars: Episode III - Revenge of the Sith 3D"; year = 2005; // <-- date du premier film de la liste }, // <-- fin des données du premier film de la liste { // <-- deuxième film de la liste "abridged_cast" = (); ... "mpaa_rating" = "PG-13"; posters = { original = "http://content9.flixster.com/movie/11/17/38/11173843_ori.jpg"; }; runtime = 127; synopsis = ""; title = "Star Trek"; year = 2009; }, ... ); total = 1319; }
En analysant la structure précédente, il est possible d’énoncer les axiomes suivants:
Ainsi, pour obtenir le titre du premier film de la liste nous écrirons:
Et pour l’année:
QUESTION: Comment obtenir l’affiche originale du deuxième film?
Action 1.3 – Affichons le titre des films reçus (à la fin de la méthode: ‘rechercherFilms’)
// Afficher le titre des films let films = self.tableauDesFilms["movies"] as! Array<Dictionary<String, AnyObject>> var _titre = "" var _annee = 0 for courant in films { if let titre = courant["title"] as? String { _titre = titre } else { _titre = "n/a" } if let annee = courant["year"] as? Int { _annee = annee } else { _annee = -1 } println("Titre du film: \(_titre) - date:\(_annee)") } // for ... in films
ou bien ainsi:
// Note: Attention, ne valide pas la présence du champ 'movies' for courant in tableauDesFilms["movies"] as! Array<Dictionary<String, AnyObject>> { if let titre = courant["title"] as? NSString { print("Titre du film: \(titre)") } if let annee = courant["year"] as? NSNumber { println(" - date:\(annee)") } } // for ... in films
Remarquer qu’il y a une zone de recherche en haut à droite de l’application. Nous allons utiliser la méthode de délégation ‘textFieldShouldReturn’ du ‘UITextField’ pour relancer la requête vers l’api de rottenTomatoes.
Action 2.1 – Ajoutons le code suivant à la méthode ‘textFieldShouldReturn’ de la classe contrôleur de la scène principale.
func textFieldShouldReturn(textField: UITextField) -> Bool { println("textFieldShouldReturn") // Convertir la chaine en format 'escaped' pour le web, par exemple, remplacer les espaces par %20, ... let escapedText = textField.text.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)! println("escapedText = \(escapedText)") // Relancer la requete vers rottenTomatoes rechercherFilms(escapedText) // Actualiser les cellules du UICollectionView collectionDeFilms.reloadData() //---------------------------------- textField.resignFirstResponder() progression.stopAnimating() return true } // textFieldShouldReturn
Action 2.2 – Testons l’application
Nous savons qu’il est possible de créer un objet de type ‘UIImage’ à partir du nom d’un fichier livré avec l’application de la façon suivante:
UIImage(Named: »pochette.jpg »)
Mais lorsque que ce fichier est stocké dans le réseau Internet il faut procéder ainsi:
UIImage(data:NSData.dataWithContentsOfURL(NSURL(string: urlImage), options: nil, error: nil))
Note: La qualité de l’expérience utilisateur va être affectée par le chargement d’un grand nombre d’images. Plus tard, nous proposerons une solution à ce problème.
Action 3.1 – Ajoutons le code suivant à la méthode ‘cellForItem…’ du fichier ViewController.m »
// ************************************************************************************************* func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell // ******************************************************* { var celluleCourante = collectionView.dequeueReusableCellWithReuseIdentifier("modeleCellule", forIndexPath: indexPath) as! CelluleRotten /// *** Temporaire: À effacer! // celluleCourante.titre!.text = "Cellule no: \(indexPath.row)" if let films = tableauDesFilms["movies"] as? Array<AnyObject> { celluleCourante.titre.text = (films[indexPath.row] as! Dictionary<String, AnyObject>)["title"] as? String // Utilisation d'un NSString pour avoir accès à la méthode NSString.rangeOfString() var urlImage = ((films[indexPath.row] as! Dictionary<String,AnyObject>)["posters"] as! Dictionary<String,NSString>)["original"]! // Patch ajoutée 2015.10.01 \ - rotten ne fournit plus de grosses images dans le feed json // Il faut maintenant faire un peu plus de travail /* http://resizing.flixster.com/AhKHxRwazY3brMINzfbnx-A8T9c=/54x80/dkpu1ddg7pbsk.cloudfront.net/movie/11/13/43/11134356_ori.jpg ... devient ... http://dkpu1ddg7pbsk.cloudfront.net/movie/11/13/43/11134356_ori.jpg */ let posDepart = urlImage.rangeOfString("dkpu1ddg7pbsk.cloudfront.net").location if posDepart != NSNotFound { urlImage = "http://" + urlImage.substringWithRange(NSRange(location: posDepart, length: urlImage.length - posDepart)) } // Version bloquante de la requête celluleCourante.filmImage.image = UIImage(data:NSData(contentsOfURL: NSURL(string: urlImage as! String)!, options: nil, error: nil)!) } return celluleCourante; } // cellForItemAtIndexPath
Action 3.2 – Corrigeons la valeur de retour de la méthode ‘numberOfItems…’
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { // films disponibles? if let films = tableauDesFilms["movies"] as? Array<Dictionary<String, AnyObject>> { return films.count } else { return 0 } } // numberOfItemsInSection
Action 3.3 – Ajoutons le code suivant à la méthode ‘prepareForSegue’
// Passer les info à la scène de détail // ************************************************************************************************* override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { var vc = segue.destinationViewController as DetailFilmViewController // Passer les informations de la sélection courante à la scène de détail let selectionCourante = collectionDeFilms.indexPathForCell(sender as! UICollectionViewCell)?.row vc.detailFilm = (tableauDesFilms["movies"] as! Array)[selectionCourante!] as Dictionary<String, AnyObject> } // // Passer les info à la scène de détail
Action 3.4 – Augmentons le nombre maximum de films pour la requête vers l’api de rottenTomatoes
// ViewController.swift let NB_FILMS = "50"
Action 3.5 – Renseignons quelques champs de la scène ‘Détail’
self.detailTitre.text = detailFilm["title"] as NSString println("Sélection courante: \(detailFilm)") if let df = detailFilm { self.detailTitre.text = df["title"] as? String // Tester l'année... if let temp = df["year"] as? Int { self.detailAnnee.text = temp.description } // Tester la durée if let temp = df["runtime"] as? Int { self.detailDuree.text = temp.description } }
Action 3.6 – Testons l’application. Vous allez remarquer un important manque de réponse de l’application.
Projet – RottenTomatoes version swift – solution étapes 1 a 3
Présentement, les requêtes web de l’application sont bloquantes, C-A-D que l’application attend la réponse de la requête avant de passer à la ligne de code suivante. Cela a pour effet de bloquer aussi l’interactivité de l’application.
Le ‘framework’ cocoa nous propose la méthode de classe ‘NSURLConnection.sendAsynchronousRequest‘ pour lancer une requête URL non bloquante:
NSURLConnection. sendAsynchronousRequest(
UneRequete,
queue: uneFileExecution,
completionHandler:UnBlocDeCodeAExecuter // à la réception des données
)
Le bloc de code (fonction anonyme, fonction en ligne) sera copié en file (queue) et exécuté, en parallèle avec l’application, lorsque les données seront obtenues.
Un bloc de code a la syntaxe suivante:
{ (param1: Type, param2:Type?,…) -> Void in
if let x = param2 {
…
}
} // fin du bloc
Le paramètre completionHandler de NSURLConnection. sendAsynchronousReques attend un bloc dont la signature est:
{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in
}
Présentement, nous utilisons la syntaxe suivante pour lancer une requête vers l’api de rottenTomatoes:
... // Obtenir les données en format brut NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:URLFilm]]; ... // Convertir les données brutes en format JSON tableauDesFilms = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; ...
Action 4.1 – Modifions la méthode ‘rechercherFilms’ pour rendre la requête non bloquante
// ************************************************************************************************* func rechercherFilms(film:String) -> Bool { println("rechercherFilms: \(film)") progression.startAnimating() let adresseURL = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=\(CLE_API)&q=\(film)&page_limit=\(NB_FILMS)" // Requete via URL non bloquante let url: NSURL = NSURL(string: adresseURL)! let requete: NSURLRequest = NSURLRequest(URL: url) // Obtenir la file courante let queue:NSOperationQueue = NSOperationQueue.currentQueue()! // Attention à: NSOperationQueue() NSURLConnection.sendAsynchronousRequest(requete, queue: queue, completionHandler:{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in if let err = error { println("Erreur avec la requête: \(err)") } // if err else { // !err avec la requête // Note: Il faut utiliser self.nomVariable lorsque nous sommes dans une fn de type 'closure' self.tableauDesFilms = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as! Dictionary<String, AnyObject> // println("Résultat de la requête: \(self.tableauDesFilms)") // Afficher le titre des films let films = self.tableauDesFilms["movies"] as! Array<Dictionary<String, AnyObject>> var _titre = "" var _annee = 0 for courant in films { if let titre = courant["title"] as? String { _titre = titre } else { _titre = "n/a" } if let annee = courant["year"] as? Int { _annee = annee } else { _annee = -1 } println("Titre du film: \(_titre) - date:\(_annee)") } // for ... in films // Recharger le CollectionView self.collectionDeFilms.reloadData() self.nbResultat.text = "\(films.count)" self.progression.stopAnimating() } // !err } // completionHandler ) // sendAsynchronousRequest return true } // rechercherFilms
Action 4.2 – Testons l’application.
Vous allez remarquer que la scène principale est affichée immédiatement. Précédemment, la scène était affichée suite au traitement de la requête web qui bloquait l’exécution de l’application.
Par contre, le chargement des pochettes de films est encore bloquant.
Utilisons la même technique pour charger les images via le réseau Internet.
Action 4.3 – Dans la méthode ‘cellForItemAtIndexPath‘, remplaçons la ligne de code qui charge l’image via une URL par:
// ************************************************************************************************* func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { var celluleCourante = collectionView.dequeueReusableCellWithReuseIdentifier("modeleCellule", forIndexPath: indexPath) as! CelluleRotten // Vérifier s'il la clé 'movie' est présente dans le dictionnaire. if let films = tableauDesFilms["movies"] as? Array<AnyObject> { celluleCourante.titre.text = (films[indexPath.row] as! Dictionary<String, AnyObject>)["title"] as? String // Utilisation d'un NSString pour avoir accès à la méthode NSString.rangeOfString() var urlImage = ((films[indexPath.row] as! Dictionary<String,AnyObject>)["posters"] as! Dictionary<String,NSString>)["original"]! // Patch ajoutée 2015.10.01 \ - rotten ne fournit plus de grosses images dans le feed jason // Il faut maintenant faire un peu plus de travail /* http://resizing.flixster.com/AhKHxRwazY3brMINzfbnx-A8T9c=/54x80/dkpu1ddg7pbsk.cloudfront.net/movie/11/13/43/11134356_ori.jpg ... devient ... http://dkpu1ddg7pbsk.cloudfront.net/movie/11/13/43/11134356_ori.jpg */ let posDepart = urlImage.rangeOfString("dkpu1ddg7pbsk.cloudfront.net").location if posDepart != NSNotFound { urlImage = "http://" + urlImage.substringWithRange(NSRange(location: posDepart, length: urlImage.length - posDepart)) } // Version bloquante de la requête // celluleCourante.filmImage.image = UIImage(data:NSData(contentsOfURL: NSURL(string: URLImageCorrigee)!, options: nil, error: nil)!) // Version non bloquante de la requête // Utiliser un 'place holder' celluleCourante.filmImage.image = UIImage(named: "loading.gif") // Préparer et lancer la requête let requeteImage = NSURLRequest(URL: NSURL(string:urlImage as! String)!) let queue:NSOperationQueue = NSOperationQueue.currentQueue()! // Attention à: NSOperationQueue() NSURLConnection.sendAsynchronousRequest(requeteImage, queue: queue, completionHandler:{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in if let err = error { println("Erreur avec la requête: \(err)") } // if err else { celluleCourante.filmImage.image = UIImage(data: data) } // for error } // completionHandler ) // NSURLConnection.sendAsynchronousRequest } // if let films = tableauDesFilms["movies"] return celluleCourante } // cellForItemAtIndexPath
Action 4.4 – À vous de modifier la classe de la scène ‘détail’ pour que le chargement des quatre affiches ne soit plus bloquant.
Action 4.5 – À vous de renseigner les champs de la scène ‘détail’
Projet – RottenTomatoes version swift – projet final sans detail
(Après un dur labeur, l’aisance)
Il existe, dans le réseau Internet, des classes Swift ‘open source’ qui facilitent le travail avec une structure JSON.
Une de ces classes est proposée par Pinglin Tang. Elle est disponible ici (version 2.2.0 pour Xcode 6.4):
https://github.com/SwiftyJSON/SwiftyJSON
Cette classe JSON s’utilise de la façon suivante:
func testerJSON(){ println("func testerJSON()") let film="star" let adresseURL = "http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=\(CLE_API)&q=\(film)&page_limit=\(NB_FILMS)" let url = NSURL(string: adresseURL) // Obtenir les données en format brut et les convertir en JSON let data = NSData(contentsOfURL:url!, options: nil, error: nil) if let resultat = data { println("L'URL a retourné des données!") var error:NSError? // Le plaisir commence ici: let x = JSON(data: resultat, options: NSJSONReadingOptions.MutableContainers, error: &error) // et voilà! println(x["movies"][0]["title"]) } else { println("Erreur: l'URL n'a pas retourné de données!") } } // func testerJSON()
Autres exemples d’utilisation:
// Obtenir le premier rôle du troixième acteur du premier film
println(donneesJSON[« movies »][0][« abridged_cast »][2][« characters »][0])//Extraire un élément de type Array:
let films = donneesJSON[« movies »].arrayValue
//Boucler sur les éléments du tableau
if let films = donneesJSON[« movies »].arrayValue {
for film in films { … }
La façon la plus simple de la rendre la classe JSON disponible dans un projet Swift est d’ajouter les fichiers ‘SwiftyJSON.swift’ et ‘SwiftyJSON.h’ au projet.
Action – À faire en lab:
En utilisant la classe JSON, il faut afficher le titre et le nom des acteurs de tous les films:
Par exemple,
------------------------------- Film: Star Wars: Episode III - Revenge of the Sith 3D Acteur: Ewan McGregor Acteur: Natalie Portman Acteur: Hayden Christensen Acteur: Ian McDiarmid Acteur: Samuel L. Jackson ------------------------------- Film: Star Acteur: Kumar Gaurav Acteur: Rati Agnihotri Acteur: Saeed Jaffrey Acteur: Raj Kiran Acteur: A.K. Hangal ------------------------------- Film: Star Trek Into Darkness Acteur: Chris Pine Acteur: Zachary Quinto Acteur: Zoe Saldana Acteur: Karl Urban Acteur: Simon Pegg
Voici une liste d’APIs disponibles via le web:
http://www.programmableweb.com/apis/directory
http://query.yahooapis.com/v1/public/yql?q=select%20item%20from%20weather.forecast%20where%20location%3D%22CAXX0301%22&format=json
Ce qui donne la météo pour Montréal (CAXX0301)
Voir: http://developer.yahoo.com/yql/
http://wxdata.weather.com/wxdata/search/search?where=montr
http://query.yahooapis.com/v1/public/yql?q=select%20%2a%20from%20yahoo.finance.quotes%20where%20symbol%20in%20%28%22YHOO%22%2C%22AAPL%22%2C%22GOOG%22%2C%22MSFT%22%29%0A%09%09&env=http%3A%2F%2Fdatatables.org%2Falltables.env&format=json
Ce qui donne les cotes de Yahoo, d’Apple et d’Amazon et de microsoft.
http://finance.yahoo.com/webservice/v1/symbols/allcurrencies/quote?format=json
http://api.tou.tv/v1/toutvapiservice.svc/json/GetPageRepertoire
Ce qui donne le répertoire de tou.tv
Voir: https://code.google.com/p/tou-tv-for-boxee/wiki/Api
Note: L’image sur cette page est la propriété intellectuelle de Flixster.
Document par Alain Boudreault (c) 2013-2015 – révision du 2015.10.16 (pour Xcode 6.4)