Projet: Les RottenTomatoes – version Swift

Utilisation d’une ‘API’ web

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.

Éléments de contenu

 

tomato1

voir note au bas


Pré-requis:

  1. Avoir complété le tutoriel TIMFlix ou bien avoir une maitrise de l’objet UITableView + segue + passage de paramètres entre les scènes d’une app.
  2. S’inscrire sur le site de rottentomatoes
  3. Obtenir une clé développeur

 
 
 
Vidéo du résultat final:

 
 


API web d’accès à une base de données

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″

 


Programmer une API Web

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’:
apitim_01
 
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

 


Retour sur les tableaux et les optionnelles

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,
retour.tableau.01
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.
 


Programmer un app avec l’API TIM

 
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 !

 

Boucler sur les éléments du tableau.  Note: le tableau ‘NSArray’ contient des éléments de type ‘NSDictionary’.

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.
...

 

Créer un tableau à partir d’une structure JSON

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()

 

Et si nous passions par une classe personnalisée?

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’
 


API de rottenTomatoes

 
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

 


Étape 1 – Obtenir des données du web

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’.
rotten_capture_01
 
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:

  1. Le dictionnaire contient un tableau de films à l’élément dictionnaire[« movies »].
  2. Toutes les informations d’un film sont disponibles à dictionnaire[« movies »][indiceDuFilm].
  3.  [« movies »][indiceDuFilm] retourne un dictionnaire
  4. Une information sur un film est disponible à dictionnaire[« movies »][indiceDuFilm] [« nomChamp »].

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

 


Étape 2 – Actualiser la requête suite à ‘textFieldShouldReturn’

 
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.
 
rotten-002
 
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
 

 


Étape 3 – Renseigner les  cellules du UICollectionView

 
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
 


Étape 4 – Améliorer l’expérience utilisateur

 
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


Simplifier l’utilisation de JSON

(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

 
 


Autres APIs

 
Voici une liste d’APIs disponibles via le web:
 

Répertoire d’APIs web

http://www.programmableweb.com/apis/directory
 

Météo sur Yahoo

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/

Trouvez le UID d’une ville

http://wxdata.weather.com/wxdata/search/search?where=montr
 

Cotes de la bourse sur Yahoo

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.

Les taux de change

http://finance.yahoo.com/webservice/v1/symbols/allcurrencies/quote?format=json

tou.tv

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
 

API Apple (iTune, app store, …)

http://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html
 

GiantBomb (jeux)

http://www.giantbomb.com/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)