TIM.Ze.Games

Utilisation d’une ‘API’ web

Révision du 2017.01.04 – conversion vers Xcode 8.0 et utilisation de Alamofire 
Dans le tutoriel TIMFlix ou ‘Les Amis de la science« , 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

Chameleon-1
Pré-requis:
Avoir complété le tutoriel TIMFlix ou bien le laboratoire ‘Les amis de la science‘, ou bien avoir une maitrise de l’objet UITableView + segue + passage de paramètres entre les scènes d’une app.
 
 
Vidéo du résultat final:


Partie 1 – 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 PHPASPPython, …,  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;
TexteXMLJSONCSV 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″, »Hiv17″
« Gameloft », »1″, »Hiv17″


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 le résultat texte suivant:

clef = nom_ajout, valeur = Alain
clef = created_at, valeur = 2014-11-01 05:11:43
clef = categorie, valeur = 3
clef = adresse_ip, valeur = 24.200.185.163
clef = pensee_texte, valeur = Il etait une fois un gars …
clef = pensee_auteur, valeur = Moi
clef = pensee_lien_image, valeur = http://www.imagesdoc.com/var/bayard/storage/images/smk/images-doc/images/images-doc-plus/que-vois-tu/photo-mystere-2-image-3/24513202-1-fre-FR/Photo-mystere-2-Image-3.jpg

 lien de test
API.TIM.04 – Voici un script PHP qui retourne le résultat en 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 "<!-- Liste générée par l'API TIM le ".date('Y-m-d h:m:s')." -->\n";
echo "<!-- (c) 2014-2016 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>";

Note: Voir la structure d’un fichier ‘plist’ sous Xcode:
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 2016-11-02 09:11:55 -->
<!-- (c) 2014-2016 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 2016.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 2016.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
Exemple php complet:

<?php
// Fichier:      apitim.php
// Par:          Alain Boudreault
// Date:         2016.10.23
// Description:
// -------------------------------------------------------------
$servername = "localhost";
$username   = "votreAcces";
$password   = "votreMotDePasse";
$dbname     = "votreBD";
$mode   = isset($_GET["mode"]) ? $_GET["mode"] : "all";
$quant  = isset($_GET["quant"]) ? $_GET["quant"] : "2";
$format = isset($_GET["format"]) ? $_GET["format"] : "texte";
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}
$sql = "SELECT `nom_ajout` ,  `created_at`, `categorie`, `adresse_ip` , `pensee_texte`,  `pensee_auteur`,  `pensee_lien_image`  FROM penseesdujours";
if ($mode == "all") {
    $sql = "SELECT `nom_ajout` ,  `created_at`, `categorie`, `adresse_ip` , `pensee_texte`,  `pensee_auteur`,  `pensee_lien_image` FROM penseesdujours";
}  // mode=all
if ($mode == "rnd") {
    $sql = "SELECT `nom_ajout` ,  `created_at`,`categorie`, `adresse_ip` , `pensee_texte`,  `pensee_auteur`,  `pensee_lien_image` FROM penseesdujours ORDER BY RAND() LIMIT $quant";
} // mode=rnd
if ($mode == "adulte") {
    $sql = "SELECT * FROM (SELECT `categorie`,`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";
    echo $sql . "\n\n";
} // mode=adulte
$res = $conn->query($sql);
// En format plist
if ($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 "<!-- Liste générée par l'API TIM le ".date('Y-m-d h:m:s')." -->\n";
  echo "<!-- (c) 2014-2016 par Alain Boudreault -->\n";
  echo "<array>\n";
  while($r = $res->fetch_assoc()) {
    $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>";
} // format = plist
// En format texte
if ($format == "texte") {
  // Liste simple
  while($row = $res->fetch_assoc()) {
    foreach ($row as $key => $value) {
       echo "clef = ".$key.", valeur = ".$value."<br>\n";
     } // foreach
  } // while
} // if format == texte
// En format json
if ($format == "json") {
  // tableau du résultat final
  $resultat = array();
  $resultat['info'] = array("API_TIM" => "version 2016.10.01", "type_requete" => "json");
  while($r = $res->fetch_assoc()) {
    // 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, JSON_PRETTY_PRINT);
} // if format == json
$conn->close();
//   echo phpinfo();
?>

Partie 2 – Utilisation de l’API de Yahoo finance

Étant donné le lien suivant:
Obtenir cotes de la bourse
Nous obtenons la structure suivante:

{
    "query":{  // Dictionary<String, Any>
             "count":4,
             "created":"2016-11-01T15:53:16Z",
             "lang":"fr-ca",
             "results": {  // Dictionary<String, Any>
                  // *** Tableau des cotes
		  "quote":[ // Array<Dictionary<String, Any>>
		    // ****  Premier élément
	          {     "symbol":"YHOO",
   	                "Ask":"40.89",
     		        "AverageDailyVolume":"10498500",
        	        "Bid":"40.86",
          	        "AskRealtime":null,
                    "BidRealtime":null,
                    "BookValue":"36.39",
                    "Change_PercentChange":"-0.23 - -0.55%",
                    "Change":"-0.23",
                    "Commission":null,
                    "Currency":"USD",
                    "ChangeRealtime":null,
                    "AfterHoursChangeRealtime":null,
                    "DividendShare":null,
                    "LastTradeDate":"10/31/2016",
                    "TradeDate":null,
                    "EarningsShare":"-5.11",
                    "ErrorIndicationreturnedforsymbolchangedinvalid":null,
                    "EPSEstimateCurrentYear":"0.58",
                    "EPSEstimateNextYear":"0.62",
                    "EPSEstimateNextQuarter":"0.14",
                    "DaysLow":null,
                    "DaysHigh":null,
                    "YearLow":"26.15",
                    "YearHigh":"44.92",
                    "HoldingsGainPercent":null,
                    "AnnualizedGain":null,
                    "HoldingsGain":null,
                    "HoldingsGainPercentRealtime":null,
                    "HoldingsGainRealtime":null,
                    "MoreInfo":null,
                    "OrderBookRealtime":null,
                    "MarketCapitalization":"39.78B",
                    "MarketCapRealtime":null,
                    "EBITDA":"90.38M",
                    "ChangeFromYearLow":"15.40",
                    "PercentChangeFromYearLow":"+58.89%",
                    "LastTradeRealtimeWithTime":null,
                    "ChangePercentRealtime":null,
                    "ChangeFromYearHigh":"-3.37",
                    "PercebtChangeFromYearHigh":"-7.50%",
                    "LastTradeWithTime":"4:00pm - <b>41.55</b>",
                    "LastTradePriceOnly":"41.55",
                    "HighLimit":null,
                    "LowLimit":null,
                    "DaysRange":null,
                    "DaysRangeRealtime":null,
                    "FiftydayMovingAverage":"42.83",
                    "TwoHundreddayMovingAverage":"39.65",
                    "ChangeFromTwoHundreddayMovingAverage":"1.90",
                    "PercentChangeFromTwoHundreddayMovingAverage":"+4.78%",
                    "ChangeFromFiftydayMovingAverage":"-1.28",
                    "PercentChangeFromFiftydayMovingAverage":"-2.98%",
                    "Name":"Yahoo! Inc.",
                    "Notes":null,
                    "Open":null,
                    "PreviousClose":"41.78",
                    "PricePaid":null,
                    "ChangeinPercent":"-0.55%",
                    "PriceSales":"8.04",
                    "PriceBook":"1.15",
                    "ExDividendDate":null,
                    "PERatio":null,
                    "DividendPayDate":null,
                    "PERatioRealtime":null,
                    "PEGRatio":"-124.19",
                    "PriceEPSEstimateCurrentYear":"71.64",
                    "PriceEPSEstimateNextYear":"67.02",
                    "Symbol":"YHOO",
                    "SharesOwned":null,
                    "ShortRatio":"4.25",
                    "LastTradeTime":"4:00pm",
                    "TickerTrend":null,
                    "OneyrTargetPrice":"45.14",
                    "Volume":"10108",
                    "HoldingsValue":null,
                    "HoldingsValueRealtime":null,
                    "YearRange":"26.15 - 44.92",
                    "DaysValueChange":null,
                    "DaysValueChangeRealtime":null,
                    "StockExchange":"NMS",
                    "DividendYield":null,
                    "PercentChange":"-0.55%"},
                    // *** Deuxième élément
                    {"symbol":"AAPL","Ask":"113.60", ... }
		] // **** Fin du tableau des cotes
	   }  // results
     } // query
}

 


Action API.TIM.01 – Dans un nouveau projet, ajoutons le code suivant:

//  ViewController.swift
//  Ajouter ceci dans le fichier info.plist
/*
 <key>NSAppTransportSecurity</key>
 <dict>
 <key>NSAllowsArbitraryLoads</key>
 <true/>
 </dict>
 */
import UIKit
class ViewController: UIViewController {
    // let URLYahooFinance = "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"
    // requete = http://query.yahooapis.com/v1/public/yql?q=select * from yahoo.finance.quotes where symbol in ("MSFT","FB","INTC","HPQ","AAPL","AMD","COKE")&env=http:/datatables.org/alltables.env&format=json
    private var dic_resultats  = Dictionary<String, Any>()
    private var _listeDesItems = Array<Dictionary<String, Any>>()
    // URL vers l'API finance de Yahoo
    let YahooFinanceURLpart1    = "http://query.yahooapis.com/v1/public/yql?q="
    let requeteSQL              = "select * from yahoo.finance.quotes where symbol in ("
    let porteFeuille = "\"MSFT\",\"FB\",\"INTC\",\"HPQ\",\"AAPL\",\"AMD\",\"COKE\""
    let YahooFinanceURLpart2    = ")&env=http://datatables.org/alltables.env&format=json"
    var URLYahooFinance         = ""
    func obtenirLesDonnées(_ url:String) {
        let uneURL = URL(string: url)!  //Danger!
        /// Exécuter le traitement suivant en parallèle
        /// DispatchQueue.main.async ( execute: {
        if let _données = NSData(contentsOf: uneURL) as? Data {
            do {
                let json = try JSONSerialization.jsonObject(with: _données, options: JSONSerialization.ReadingOptions()) as? Dictionary<String, Dictionary<String, Any>>
                print("Conversion JSON réussie")
                self.dic_resultats = json!
                //print(self.dic_resultats)
                // Créer un tableau à partir du champ 'resultats'
                if let listeDesItems = ((self.dic_resultats["query"] as? Dictionary<String, Any>)?["results"]as? Dictionary<String, Any>)?["quote"] as? Array<Dictionary<String, Any>> {
                    self._listeDesItems = listeDesItems
                    print("Liste des items:\n\(self._listeDesItems)")
                    // self.collectionDesItems.reloadData()
                }
                // print(json)
            } catch {
                print("\n\n#Erreur: Problème de conversion json:\(error)\n\n")
            } // do/try/catch
        } else
        {
            print("\n\n#Erreur: impossible de lire les données via:\(self.URLYahooFinance)\n\n")
        } // if let _données = NSData
        /// }) // DispatchQueue.main.async
    } // obtenirLesDonnées(_ url:String)
    override func viewDidLoad() {
        super.viewDidLoad()
        // Construire l'URL de l'API Yahoo à partir du portefeuille local
        URLYahooFinance = YahooFinanceURLpart1 + (requeteSQL + porteFeuille).addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics)! + YahooFinanceURLpart2
        print(URLYahooFinance)
        obtenirLesDonnées(URLYahooFinance)
    } // viewDidLoad()
}

Analyse du code précédent:
Action: Testons l’application
Résultat obtenu:

2016-11-05 09:40:54.698663 Tester Yahoo finance[8399:131637] [] tcp_connection_get_statistics DNS: 3ms/9ms since start, TCP: 88ms/103ms since start, TLS: 0ms/0ms since start
Conversion JSON réussie
Liste des items:
[["Change_PercentChange": -0.50 - -0.84%, "EPSEstimateCurrentYear": 2.97, "LastTradePriceOnly": 58.71, "ChangePercentRealtime": <null>, "Open": 58.65, "HoldingsGainRealtime": <null>, "DaysLow": 58.52, "PERatio": 28.12, "PERatioRealtime": <null>, "OneyrTargetPrice": 63.81, "FiftydayMovingAverage": 58.21, "AskRealtime": <null>, "SharesOwned": <null>, "DaysHigh": 59.28, "Name": Microsoft Corporation, "Change": -0.50, "DividendYield": 2.62, "YearHigh": 61.37, "DividendPayDate": 12/8/2016, "YearRange": 48.04 - 61.37,
...

Action API.TIM.02 –  Affichons le nom de l’action ainsi que le prix demandé:

    func afficherActions() {
        print("---------------------------------------")
        for action in _listeDesItems{
            if let _nom = action["Name"], let _prix = action["Ask"] {
                print("Action: \(_nom), prix: \(_prix)")
            }
        } // for action in
        print("---------------------------------------")
    } // afficherActions()

Résultat obtenu:

---------------------------------------
Action: Microsoft Corporation, prix: 58.74
Action: Facebook, Inc., prix: 120.72
Action: Intel Corporation, prix: 34.12
Action: HP Inc. Common Stock, prix: 14.71
Action: Apple Inc., prix: 108.85
Action: Advanced Micro Devices, Inc., prix: 6.57
Action: Coca-Cola Bottling Co. Consolid, prix: <null>
---------------------------------------

Et pour une lecture des données à chaque n sec:

    func viewDidLoad() {
    ...
        Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.doTimer), userInfo: nil, repeats: true)
    } // viewDidLoad()
    func doTimer(){
        obtenirLesDonnées(URLYahooFinance)
        afficherActions()
        ///  Actualiser une collectionView
        ///  tableViewActions.reloadData()
    }

Exécution d’une requête en parallèle avec l’application:

// Retirer le commentaire des lignes suivantes:
  DispatchQueue.main.async ( execute: {
  ...
 }) // DispatchQueue.main.async

Analysons le résultat suivant:

http://query.yahooapis.com/v1/public/yql?q=select%20%2A%20from%20yahoo%2Efinance%2Equotes%20where%20symbol%20in%20%28%22MSFT%22%2C%22FB%22%2C%22INTC%22%2C%22HPQ%22%2C%22AAPL%22%2C%22AMD%22%2C%22COKE%22)&env=http://datatables.org/alltables.env&format=json
---------------------------------------
---------------------------------------
2016-11-05 10:23:11.400069 Tester Yahoo finance[11698:169904] subsystem: com.apple.BackBoardServices.fence, ca

La liste est vide la première fois.
Raison: Nous affichons le contenu du tableau avant d’avoir reçu la réponse de l’API.
Précédemment, l’exécution du programme bloquait à la lecture des données à partir d’Internet.
Maintenant, avec ‘ DispatchQueue.main.async ( execute: ‘, la lecture des données est faite en arrière plan et notre programme principal continu les traitements.

obtenirLesDonnées(URLYahooFinance)  // Envoyé en arrière plan
afficherActions()                   // *** N'attend plus après l'instruction précédente.

Avec cette technique, il faut traiter l’affichage des données et l’actualisation des composants MVC à l’intérieur du bloc de code (inline function, fonction anonyme).
Dans ce cas, les variables doivent-être précédées de ‘self’; self.monCollectionView.reloadData().
Voir le code de la solution:
Télécharger la solution


Laboratoire

Choix 1 – Valeurs de conversion des devises en fonction du dollar américain.

Obtenir la valeur des monnaies en fonction de USD à partir de l’API suivante:
http://finance.yahoo.com/webservice/v1/symbols/allcurrencies/quote?format=json
Action: Écrire une application qui affiche, dans un UITableView, le Code (symbol) des devises ainsi que le prix (price).  Actualiser les données à chaque 15 secondes.
 

Choix 2 (défi plus élevé) – La température en temp réel de 10 villes 

Obtenir la température à partir de l’API suivante:
Température de 10 villes
Action: Écrire une application qui affiche, dans un UITableView, le nom de la ville ainsi que la température actuelle.  Actualiser les données à chaque 15 secondes.


Partie 3 – API TIM.Ze.Game

Le site ‘TIM.Ze.Game’ propose une API d’accès aux données qu’il compile.  Ces données nous renseignent sur des pochettes de jeux,  les critiques, les suiveux, …
Pour interroger l’api, il suffit de suivre l’URL suivante:

http://api-tim.ze.game?apikey=VOTRECLE&q=MODELE_DE_RECHERCHE&quant=NB_PAGES&format=json

Comme par exemple: http://prof-tim.cstj.qc.ca/cours/xcode/sources/timgames/api.timgames.php?apikey=ceciEstUltraSecret&q=Star&quant=50&format=json

La requête précédente devrait nous retourner une structure JSON contenant une liste de 50 pochettes dont la chaîne ‘Star’ apparaît dans le titre du jeu.
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:
if let NSData(contentsOf: uneURL) as? Data {
do {
let json = try JSONSerialization.jsonObject(with: _données, options: JSONSerialization.ReadingOptions()) as? Dictionary<String, Any>
}


Étape 1 – Prise en main du projet de départ – lecture des données

Action 1.1 – Ouvrons le projet Projet – TIM.Ze.Game – depart et exécutons l’application
TIM.ze.Game-cap30
Action 1.1.2 – Examinons le code de la classe ‘Intro’; animations et trame sonore:

// Voici comment animer une propriété:
titre1.alpha  = 0
UIView.animate(withDuration: 3.0,
                      delay: 0.0,
                    options: UIViewAnimationOptions.curveEaseInOut,
                 animations: { self.titre1.alpha = 1.0 },
                 completion: nil)
// Voici comment jouer un fichier audio:
var player: AVAudioPlayer?
func jouerIntro() {
   let url = Bundle.main.url(forResource: "fichier", withExtension: "mp3")!
   do {
        player = try AVAudioPlayer(contentsOf: url)
        guard let player = player else { return } // autre façon de faire un if let _ {} else {}
        player.prepareToPlay()
        player.play()
       } catch let error {
            print(error)
       } // do/try/catch
} // func jouerIntro()

Action 1.1.3 – Ajoutons le code suivant à la méthode « obtenirDonnéesVersionBloquante » de la classe contrôleur de la scène principale.

// *** Code à insérer dans la méthode "obtenirDonnéesVersionBloquante"
        let strURL = "http://prof-tim.cstj.qc.ca/cours/xcode/sources/timgames/api.timgames.php?apikey=\(Globales.CLE_API)&q=\(chaine)&format=json&quant=\(tailleRequete.text!)"
        let uneURL = URL(string: strURL)!  //Danger!
        /// Exécuter le traitement suivant en parallèle
        /// DispatchQueue.main.async ( execute: {
        if let _données = NSData(contentsOf: uneURL) as? Data {
            do {
                let json = try JSONSerialization.jsonObject(with: _données, options: JSONSerialization.ReadingOptions()) as? Dictionary<String, Any>
                print("Conversion JSON réussie")
                self._résultatDeLaRequête = json!
                // Créer un tableau à partir du champ 'resultats'
                if let listeDesItems = self._résultatDeLaRequête["resultats"] as? Array<Dictionary<String, Any>> {
                    self._listeDesItems = listeDesItems
                    print("Liste des items:\n\(self._listeDesItems)")
                    self.collectionDesItems.reloadData()
                }
                // print(json)
            } catch {
                print("\n\n#Erreur: Problème de conversion json:\(error)\n\n")
            } // do/try/catch
        } else
        {
            print("\n\n#Erreur: impossible de lire les données via:\(strURL)\n\n")
        } // if let _données = NSData
        /// }) // DispatchQueue.main.async

Action 1.2 – Testons l’application.  Remarquez que la méthode ‘obtenirDonnéesVersionBloquante‘ est appelée dans ‘viewDidLoad’.
TIM.Ze.Game-cap20
Nous devrions obtenir le résultat suivant dans la zone ‘debug »  d’Xcode:

{
    "info": {
        "api.TIMGames": "version 2016.10.01",
        "Auteur_API": "Alain Boudreault, AKA Puyansude, AKA ve2cuy",
        "type_requete": "json",
        "recherche": "je vais ",
        "apikey": "ceciEstUltraSecret",
        "droit_auteur": "Cette API est \u00e0 l'usage exclusif des \u00e9tudiantes et \u00e9tudiants de 'Production Multim\u00e9dia sur Support' de tim.cstj.qc.ca'",
        "site_web": "http:\/\/prof-tim.cstj.qc.ca\/cours\/xcode\/wp\/index.php\/contenu\/",
        "nombre": "2",
        "addresse_IP": "192.226.187.50",
        "date": {
            "seconds": 49,
            "minutes": 43,
            "hours": 14,
            "mday": 18,
            "wday": 2,
            "mon": 10,
            "year": 2016,
            "yday": 291,
            "weekday": "Tuesday",
            "month": "October",
            "0": 1476816229
        }
    },
    "resultats": [
        {
            "titre": "Dqprzfuwkkf",
            "editeur": "TIMGames",
            "annee": 2012,
            "rang_semaine": 83,
            "pochettes": {
                "petite": "21p.jpg",
                "grande": "21.jpg"
            },
            "description": "Debwusy rcexuwj ",
            "like_facebook": 1595,
            "like_twitter": 6386,
            "cotes": {
                "critiques": 10,
                "joueurs": 9
            },
            "suiveux": [
                {
                    "nom": "Gthqynsuh",
                    "avatar": "avatar-16.jpg"
                },
                {
                    "nom": "Lsicwo q",
                    "avatar": "avatar-27.jpg"
                }
            ]
        },
        {
            "titre": "Kkroahfanaxzlcff",
            "editeur": "TIMGames",
            "annee": 1989,
            "rang_semaine": 80,
            "pochettes": {
                "petite": "4p.jpg",
                "grande": "4.jpg"
            },
            "description": "Debwusy rcexuwj ",
            "like_facebook": 5660,
            "like_twitter": 6975,
            "cotes": {
                "critiques": 2,
                "joueurs": 1
            },
            "suiveux": [
                {
                    "nom": "Kwdzpygar",
                    "avatar": "avatar-25.jpg"
                },
                {
                    "nom": "Qeoeffp",
                    "avatar": "avatar-17.jpg"
                }
            ]
        }
    ]
}

NOTE: En analysant la structure précédente, il est possible d’énoncer les axiomes suivants:

  1. L’API renvois un dictionnaire avec deux clés:  « info:Dictionary<String, Any> » et « resultats:Array<Dictionary<String, Any>> »
  2. Nous obtenons la liste des items par;  items = données[« resultats] as? Array<Dictionary<String, Any>>
  3. Le titre du premier item s’obtient par;  if let titre = items[0][« titre »] as? String {}

Action 1.3 – Affichons le titre des items reçus (à la fin de la méthode: ‘obtenirDonnéesVersionBloquante’)

       // Afficher le titre des items
       for (indice,item) in _listeDesItems.enumerated() {
            if let _titreItem = item["titre"] as? String {
                print("Titre item \(indice): \(_titreItem)")
            }
        }

Laboratoire

Afficher le nom des suiveux sous le titre des items

-----------------------------------
Titre: the Rwdqxmoqfuw
-----------------------------------
Titre: the Wgy lfypzvqiiieixtw
		suiveux: Hlslb
-----------------------------------
Titre: the Qzrgfldbscceibuhxk
		suiveux: Rguwqsqm b
		suiveux: Emdvmv

 


É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 TIM.Ze.Game.
rotten-002
Action 2.1 – Effaçons les commentaires du code de la méthode ‘textFieldShouldReturn‘ de la classe contrôleur de la scène principale.

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // print("textFieldShouldReturn")
        /* *********************************
         À compléter ....
         ********************************* */
        // TODO: Convertir la chaine en format 'escaped' pour le web. Par exemple, ' '= %20
        let escapedText = textField.text!.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics)!
        print("escapedText = \(escapedText)")
        // TODO: Relancer la requete vers l'API
        _ = obtenirDonnéesVersionBloquante(escapedText)
        //----------------------------------
        textField.resignFirstResponder()
        progression.stopAnimating()
        return true
    } // textFieldShouldReturn

Action 2.2 – Testons l’application
Résultat: La chaine de saisie de recherche a été encodé en caractères d’échappement WEB.


É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:

do {
let _data = try Data(contentsOf: _url, options: Data.ReadingOptions.alwaysMapped)
cellule.pochetteImage.image = UIImage(data: _data)
}
catch  { }

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 –  Remplaçons la méthode ‘cellForItem…’ du fichier ViewController.swift » par:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cellule = collectionView.dequeueReusableCell(withReuseIdentifier: "modeleCellule1", for: indexPath) as! CollectionViewCellPerso1
        // TODO: 5a - Renseigner les éléments d'interface de la cellule courante: titre, image, ...
        let itemCourant = _listeDesItems[indexPath.row]
        let nomFichierPetitePochette = (itemCourant["pochettes"] as? Dictionary<String, String>)?["petite"]
        let URLFichierImage = "\(Globales.URLDonnées)\(nomFichierPetitePochette!)"
        print(URLFichierImage)
        cellule.pochetteTitre.text = itemCourant["titre"] as? String
        // Version très bloquante:
        if let _url = URL(string: URLFichierImage) {
            do {
                let _data = try Data(contentsOf: _url, options: Data.ReadingOptions.alwaysMapped)
                cellule.pochetteImage.image = UIImage(data: _data)
            }
            catch  {
                print("Ligne \(#line), \(error), ### Exception: Problème avec URL: \(URLFichierImage)")
                cellule.pochetteImage.image = UIImage(named:Globales.NA_IMAGE)
            }
        } // if let _url
        // TODO: 5b - Renseigner les éléments d'interface en version non bloquante
        // Renseigner la couleur de l'entête indexPath.row modulo 2
        let couleur1 = UIColor.init(red: 140 / 255.0, green: 188 / 255.0, blue: 220 / 255.0, alpha: 1)
        let couleur2 = UIColor.init(red: 108 / 255.0, green: 145 / 255.0, blue: 168 / 255.0, alpha: 1)
        let couleur = indexPath.row % 2 == 0 ? couleur1 : couleur2
        cellule.entete.backgroundColor = couleur
        return cellule
    } // collectionView: cellForItemAt

Action 3.2 – Corrigeons la valeur de retour de  la méthode ‘numberOfItems…’

func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  return _listeDesItems.count
} // numberOfItemsInSection

Action 3.3 – Ajoutons le code suivant à la méthode ‘prepareForSegue’

    // MARK: Préparer les données pour le segue vers la scène Détails:
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let destination = segue.destination as! ViewControllerDetails
        destination.detailsItemCourant = _listeDesItems[(collectionDesItems.indexPath(for: sender as! UICollectionViewCell)?.row)!]
    }

Action 3.4 – Testez l’application.  Vous allez remarquer un important manque de réponse de l’application.


Laboratoire

Dans la scène « Détails », renseignez la grande image de la pochette et la liste des suiveux (nom et image).
 


É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.
 
Action 4.1 – Modifions la méthode ‘obtenirDonnéesVersionBloquante’ pour rendre la requête non bloquante

       /// Exécuter le traitement suivant en parallèle
       // Supprimer le commentaire des lignes suivantes:
       DispatchQueue.main.async ( execute: {
         ...
       }) // DispatchQueue.main.async

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 une technique similaire pour charger les images via le réseau Internet.
Action 4.3 – Dans la méthode ‘cellForItemAtIndexPath‘, remplaçons les lignes de code  qui charge l’image via une URL par ceci:

       cellule.pochetteImage.image = UIImage(named:Globales.LOADING_IMAGE)
       ImageViaURL.obtenirImage(urlStr: URLFichierImage, uneimage: cellule.pochetteImage)

Action 4.3.1 – Analysons la méthode : ImageViaURL.obtenirImage()

    /// **********************************************
    static func obtenirImage(urlStr:String, uneimage: UIImageView){
        // Préparer et lancer la requête
        let request = URLRequest(url: NSURL(string:urlStr ) as! URL)
        let session = URLSession.shared
        let task = session.dataTask(with: request,
                                    completionHandler: {data, response, error -> Void in
                                        if (error == nil) {
                                            DispatchQueue.main.async ( execute:
                                                {
                                                    if let _data = data {
                                                        uneimage.image = UIImage(data: _data)
                                                    } else
                                                    {
                                                        uneimage.image = UIImage(named: Globales.NA_IMAGE)
                                                    }
                                                }
                                            )  // DispatchQueue.main.async()
                                        } else { // erreur d'URL
                                            uneimage.image = UIImage(named: Globales.NA_IMAGE)
                                        }
        })
        task.resume()  // Reprendre le traitement de la session pour qu'elle puisse se terminer.
    } // obtenirImage

 
Action 4.4 – À vous de modifier la classe de la scène ‘détails’ pour que le chargement des images ne soit plus bloquant.
 


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:
https://github.com/SwiftyJSON/SwiftyJSON
La façon la plus simple de rendre la classe JSON disponible dans un projet Swift est d’ajouter les fichiers ‘SwiftyJSON.swift’ et ‘SwiftyJSON.h’ au projet.
Voici un exemple d’utilisation:
Action: Ajoutons le code suivant au projet:

    func testerJSON(){
        let strURL = "http://prof-tim.cstj.qc.ca/cours/xcode/sources/timgames/api.timgames.php?apikey=\(Globales.CLE_API)&q=&format=json&quant=50"
        /// Exécuter le traitement suivant en parallèle
            if let _données = NSData(contentsOf: URL(string: strURL)!) as? Data {
                let json = JSON(data: _données)
                if let titre = json["resultats"][0]["titre"].string {
                    print("Le titre du premier Item est \(titre)")
                }
                let imagePochette = json["resultats"][0]["pochettes"]["grande"].string
                print(imagePochette)
               for item in json["resultats"].arrayValue {
                        print("-------------------------")
                        print("Titre " + item["titre"].string!)
               }
        }
    }

Action: Testons le code
 


Laboratoire

En utilisant la classe JSON, il faut afficher le titre et le nom des suiveux de tous les items.
Par exemple,

-----------------------------------
Titre: the Zzigzbzxqwk
		suiveux: Jhasvei jp
		suiveux: Lzkrule
-----------------------------------
Titre:  the Baoa xhz hx
		suiveux: Qdhamithnb
		suiveux: Iysuycm
		suiveux: Spruqgwq
		suiveux: Deqdmnv
		suiveux: Qvwcd
		suiveux: Plssu
-----------------------------------
Titre: the Nzjlqerpmzylbd
-----------------------------------

Solution:

import Alamofire
import RxSwift
import RxCocoa
import SwiftyJSON
///
var resultat:JSON?
///
let strURL = "http://prof-tim.cstj.qc.ca/cours/xcode/sources/timgames/api.timgames.php?apikey=ceciEstUltraSecret&q=&format=json&quant=5"
Alamofire.request(strURL).response { response in // method defaults to `.get`
    debugPrint(response)
    self.resultat = JSON(data: response.data!)
    // print(self.resultat as Any)
    for item in (self.resultat?["resultats"].arrayValue)! {
        print("-------------------------")
        print("Titre " + item["titre"].string!)
        for suiveux in item["suiveux"].arrayValue {
            print("\tsuiveux: \(suiveux["nom"].stringValue)")
        } // for suiveux
    } // for item
} // Alamofire.request(strURL).response

 

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/


Document par Alain Boudreault (c) 2016