Polytech Marseille Informatique Développement mobile - TP 3 Stéphane Ayache, Nicolas Baudru Objectifs Illustrer la programmation par blocs S amuser avec la géolocalisation Découverte de Grand Central Dispatch Déployer les applications sur des terminaux ios Blocs : mise en oeuvre avec Objective-C La notion de bloc a été introduite par Apple depuis ios5 en extension aux langages C, C++ et Obj C pour simplifier l écriture et la lisibilité des programmes. Ils sont similaires aux fonctions C standard, mais en plus de code exécutable, ils peuvent également contenir des variables et utiliser ou modifier des variables partagées. Un bloc peut maintenir un ensemble d'états (données) qu'il peut utiliser pour modifier son comportement lorsqu'il est exécuté. Les blocs sont particulièrement utiles en tant que callbacks et remplacent ainsi la délégation pour certaines classes dans le SDK. En Objective C, les blocs sont considérés comme des objets. Ils peuvent être définis à l intérieur d autres méthodes, être affectés à une variable, être passés en arguments de méthodes ou peuvent être stockés dans des collections (NSArray, NSDictionary). Nous introduisons ici la syntaxe et la terminologie nécessaires à l utilisation des blocs.
Le signe ^ sert à la déclaration d une variable de bloc et à démarrer une expression de bloc. Le code suivant est un premier exemple de déclaration et utilisation d un bloc : int multiplier = 7 ; int (^myblock)( int ) = ^( int num) { return num * multiplier; }; // prints "21" NSLog ( @"%d", myblock( 3 )); N hésitez par à vous rendre sur la documentation en ligne, le guide de programmation par blocs est très instructif. https://developer.apple.com/library/ios/documentation/cocoa/conceptual/programmingwitho bjectivec/workingwithblocks/workingwithblocks.html Un premier bloc Dans le TP précédent, nous avions utilisé le service de Geocoding de Google à des fins pédagogiques : nous avons ainsi vu comment intérroger un service web avec la méthode stringwithcontentsofurl et comment traiter la réponse avec NSXMLParser. Le SDK ios permet aussi le geocoding, et de façon bien plus simple que nous l avons vu! Le code suivant montre comment parvenir au même résultat en cinq lignes : CLGeocoder *geocoder=[[ CLGeocoder alloc ] init ]; [geocoder geocodeaddressstring : @"Castellane,Marseille" completionhandler :^( NSArray *placemarks, NSError *error) { CLPlacemark *pl=[placemarks objectatindex : 0 ]; MKCoordinateRegion region= MKCoordinateRegionMakeWithDistance (pl. location. coordinate, 100, 100 ); [ mapview setregion :region animated : YES ]; }]; Dans la méthode geocodeaddressstring:completionhandler:, le premier argument est un NSString contenant l adresse recherchée et le deuxième argument est de type ^(NSArray *placemarks, NSError *error). C est donc un bloc, une fonction qui sera appelée après avoir réceptionné la réponse à la requête Castellane, Marseille. Plus besoin d intérroger le service Google, ni de traiter le résultat avec des NSString ou NSXMLParer.
Ouvrez le projet du TP précédent et modifiez le de façon intégrer cette dernière trouvaille. La complétion proposée par XCode, met en évidence les arguments des méthodes (on peut passer d un argument à l autre avec la touche Tab). Lorsque l argument de type bloc est ainsi surligné, appuyez sur Entrée : les accolades du bloc sont directement ajoutées, il ne reste plus qu à ajouter le code dans le corps du bloc. N oubliez pas d ajouter un point virgule après le crochet fermant de la fonction geocodeaddressstring:completionhandler:. Géolocalisation Nous allons ajouter une fonctionalité de géolocalisation à notre application. L application comptera la distance parcourue (depuis son dernier lancement ou réinitialisation), affichera la position courante et le tracé du parcours sur la carte. Initialisation et configuration du locationmanager L API du framework CoreLocation contient la classe CLLocationManager qui permet l accès, le démarrage et l arrêt des différentes ressources de localisation du terminal (position GPS ou autre, boussole, bluetooth). Nous devrons donc instancier un objet de cette classe. Déclarez une variable de type CLLocationManager: CLLocationManager *locationmanager; Dans la fonction ViewDidLoad de la classe ViewController, instanciez cette variable : locationmanager =[[ CLLocationManager alloc ] init ]; Parcourez la documentation ou naviguez dans les fichiers header (via un cmd + clic sur le nom d une classe) pour vous renseigner sur le fonctionnement de CLLocationManager. Vous avez trouvé le nom du protocole associé à cette classe? En effet, CLLocationManager utilise aussi la délégation pour fonctionner. Ainsi, les méthodes startupdatinglocation et stopupdatinglocation permettent respectivement de démarrer et arrêter le service de localisation, tandis que la méthode locationmanager:didupdatelocations: du protocole CLLocationManagerDelegate sera appelée à chaque fois que la position change. Dans notre cas, la classe ViewController sera le délégué de CLLocationManager. Modifiez ViewController.h en conséquence :
@interface ViewController: UIViewController < UITextFieldDelegate, CLLocationManagerDelegate > Dans la méthode viewdidload de ViewController.m, après l initialisation de locationmanager, vous pouvez configurer cet objet (regardez notamment les propriétés distancefilter et desiredaccuracy ), puis ajoutez : locationmanager. delegate = self ; Avec ios7, l accès à la position de l utilisateur ne nécessite pas d autorisation (!). Nous pouvons donc directement démarrer la géolocalisation : [ locationmanager startupdatinglocation ]; Depuis ios8, l accès à la position de l utilisateur nécessite une autorisation. Si vous utilisez une version de XCode avec le SDK ios 8, vous devrez d abord spécifier que l application effectuera cette demande : dans le panneau de configuration du projet, allez dans l onglet Info et ajoutez une entrée à la liste Custom ios Target Properties. L entrée est de type String et sa clé est NSLocationWhenInUseUsageDescription. Dans viewdidload, une fois le locationmanager configuré (regardez notamment les propriétés distancefilter et desiredaccuracy ), on vérifie le status d autorisation. S il est indeterminé, il faut faire la demande d autorisation. Si la demande a déjà été validée, on peut démarrer le service. Sinon, on affiche un message sur la console. CLAuthorizationStatus status=[ CLLocationManager authorizationstatus ]; if (status== kclauthorizationstatusnotdetermined ){ [ locationmanager requestwheninuseauthorization ]; } else if (status== kclauthorizationstatusauthorizedwheninuse ){ [ locationmanager startupdatinglocation ]; } else { NSLog ( @"Notallowedtoaccesscurrentlocation" ); } A la première exécution, la méthode de délégué locationmanager:didchangeauthorizationstatus: est appelée. Si tout est bon, c est le moment de démarrer le service de localisation. Ajoutez la méthode suivante dans ViewController.m : // ForiOS8compatibility: needtogetauthorizationforlocationtracking -( void )locationmanager:( CLLocationManager *)manager didchangeauthorizationstatus:( CLAuthorizationStatus )status{ if (status== kclauthorizationstatusauthorizedwheninuse ){ [manager startupdatinglocation ]; } }
Pour aller plus loin : https://developer.apple.com/library/ios/documentation/userexperience/conceptual/locationa warenesspg/corelocation/corelocation.html Distance parcourue Nous allons maintenant nous concentrer sur le suivi de la position de l utilisateur. Dans un premier temps, l application va compter la distance parcourue depuis le démarrage du service : Dans la classe ViewController, ajoutez la variable totaldistance de type double ; et dans viewdidload, initialisez cette variable à 0. Lorsque la position est mise à jour, la méthode locationmanager:didupdatelocations: est appelée. Le deuxième argument est un tableau qui contient des objets CLLocation rangés par ordre chronologique (le dernier est le plus récent). Vous pouvez connaître la distance en mètres entre deux positions grâce à la méthode distancefromlocation:. Il semblerait que le simulateur ne maintienne pas l historique des positions : le tableau de CLLocation n a toujours qu un seul objet. A vous donc de conserver la position précédente dans une variable oldposition. Implémentez cette méthode et affichez la distance parcourue dans le UILabel. Attention, les quelques premiers appels à cette méthode retournent des positions souvent très érronées. Il est possible de vérifier la précision estimée par les propriétés horizontalaccuracy et verticalaccuracy ; sinon, vous pouvez juste ignorer les cinq premiers appels à cette méthode. Ajoutez le suivi de la position sur la carte. Notez qu il est possible de simuler un déplacement sur le Simulateur : Menu Debug/Location/ Modifiez le comportement du UISwitch pour qu il mette en pause (démarre/arrête) la géolocalisation. Ainsi, la recherche d une adresse (geocoding) ne fonctionnera que lorsque le switch est éteint. Vous pouvez écrire Saisir une adresse : dans le UILabel lorsque le switch est éteint et cacher le UITextField lorsque le switch est allumé. Tracé du parcours Si ce n est pas déjà fait, ajoutez le protocole MKMapViewDelegate dans la déclaration de la classe ViewController, et ajoutez l instruction suivante dans viewdidload : mapview. delegate = self ;
Pour afficher le tracé du parcours sur la carte, plusieurs solutions s offrent à nous, nous utiliserons les capacités de la classe MKMapView et du protocole MKMapViewDelegate. Les objets affichables sur la carte doivent implémenter le protocole MKOverlay : nous ajouterons des MKPolyline qui implémentent ce protocole. Dans la méthode locationmanager:didupdatelocations:, construisez un objet MKPolyline à partir des deux positions précédente et courante. Ajoutez cet objet à votre MKMapView par l appel à la méthode d instance addoverlay:. Le rendu visuel de ces objets est déterminé dans un deuxième temps par l appel à la méthode du délégué MKMapViewDelegate mapview:rendererforoverlay:. Voici une façon de générer un rendu pour un objet MKPolyline : -( MKOverlayRenderer *)mapview:( MKMapView *)mapviewrendererforoverlay:( id < MKOverlay >)overlay { MKPolylineRenderer * lineview=[[ MKPolylineRenderer alloc ] initwithpolyline :overlay]; lineview. strokecolor =[ UIColor bluecolor ]; lineview. linewidth = 5 ; return lineview; } Réinitialisation du suivi par détection d une secousse Avant de terminer ce TP, nous allons ajouter une dernière fonctionalité à notre application : nous souhaitons réinititaliser le suivi et la distance parcourue en agitant le terminal ios. La réinitialisation consiste à : Remettre à zéro la variable totalmeters, Enlever les MKOverlay de la carte. Regardez bien la documentation, un objet MKMapView peut supprimer plusieurs MKOverlay passés en arguments dans un NSArray. Vous devrez donc maintenir une collection de MKOverlay préalablement ajoutés... Pour éviter une réinitialisation fortuite, la réinitialisation ne se fera que lorsque le bouton UISwitch est positionné sur Off. Ajouter un feedback visuel indiquant la réinitialisation. Plusieurs solutions s offrent à nous pour détecter la secousse de l appareil. Nous allons voir une façon naturelle de le faire, basée sur la détection de Gesture. (une autre façon, de plus bas niveau ferait appel au framework CoreMotion.) La classe ViewController hérite de UIViewController qui hérite de UIResponder. UIResponder est la classe responsable de la gestion des évènements (Touch, Motion, ). Du fait de cet
héritage, nous pouvons directement surcharger l une des méthodes de UIResponder qui nous intéresse. Allez dans la documentation et retrouvez le nom de la méthode qui capturera l évènement Shake. Ajoutez la méthode retrouvée dans la documentation et implémentez le comportement décrit précédemment. Le feedback visuel peut consister à afficher le message Réinitialisation en rouge pendant une seconde. Nous allons utiliser les fonctionalités du Framework Dispatch pour cela. Dispatch (ou GCD ) est un Framework pour accorder la concurrence entre des exécutions de blocs. L API est écrite en C, elle permet d exécuter des blocs de façon synchrone ou asynchrone dans des Threads distincts. Nous utiliserons la fonction dispatch_after pour programmer l exécution d un bloc (une seconde plus tard). La documentation de GCD et les paramètres des fonctions peuvent sembler peu claires. Pas de panique, XCode complète judicieusement l appel aux différentes fonctions, et le tout s avère finalement très simple! Appuyez sur Entrée après avoir écrit dispatch_after : magie. Dans la méthode motionended:withevent: (vous l aviez trouvé n est ce pas?) après la réinitialisation, modifiez le texte du UILabel pour afficher le message Réinitialisation en rouge. Puis, dans un bloc dispatch_after, repassez le texte et la couleur d origine (probablement Saisir une adresse ). Amélioration de l interface graphique Cette partie vise à enrichir le rendu visuel de l application en vue d une meilleure interaction avec l utilisateur. D abord, nous ajouterons une animation pour montrer/cacher le UITextField selon l état du UISwitch. Puis, nous modifierons le comportement de la carte pour, en plus de suivre la position, s oriente selon la direction de l utilisateur. Les animations apportent des effets visuels nécessaires à une meilleure interaction avec l utilisateur, permettant une meilleure prise en main, et participent ainsi à une meilleure perception générale de l application. Evidemment, les animations ne concernent que les éléments graphiques de UIKit. En particulier, certaines propriétés sont animables. Par l utilisation simple de blocs, le développeur définie l état final dans lequel l objet doit se trouver, et Core Animation se charge du rendu intermédiaire. Voici la liste des propriétés animables, d après la documentation officielle. https://developer.apple.com/library/ios/documentation/windowsviews/conceptual/viewpg_ip honeos/animatingviews/animatingviews.html
Les blocs d animation sont appelés depuis des méthodes de classe de UIVIew. Par exemple, le code suivant modifie la propriété alpha de adressetextfield pendant 0.5 seconde jusqu à la valeur 0 : [ UIView animatewithduration :.5 animations :^{ adressetextfield. alpha = 0 ; }]; Le code suivant modifie la position de adressetextfield selon l état de switchbutton : [ UIView animatewithduration :.5 animations :^{ CGRect frame= adressetextfield. frame ; frame. origin. y = switchbutton. ison? 50. : 0. ; adressetextfield. frame =frame; }]; Remarque : frame.origin.x n est pas directement assignable, il faut donc modifier toute la structure frame. Modifiez le rendu de la vue depuis le StoryBoard ; la carte prend dorénavent toute la taille de la vue ; le label s étend sur toute la largeur, collé sur le haut de l écran, avec une légère transparence ; le textfield est juste en dessous du label, étiré aussi sur toute la largeur et avec la même transparence. Ajoutez une animation de votre choix sur adressetextfield pour le rendre visible uniquement lorsque le bouton UISwitch est éteint. Veillez bien à placer initialement le textfield dans sa position cachée si l application démarre avec le UISwitch allumé.
Nousallonsmaintenantorienterlacarteenfonctiondesmouvementsdel utilisateur.pour cela,nousutiliseronslecapteurdedirection(heading)intégréau locationmanager :les méthodes startupdatingheading et stopupdatingheading fonctionnentcomme start/stopupdatinglocation et appellent laméthodededélégué locationmanager:didupdateheading: pouravertirlecontroleurd unchangementdedirection. Retrouvezcesméthodesdansladocumentation. Ajoutezledémarrage/l arrêtducapteurdedirectionenmêmetempsquele démarrage/l arrêtducapteurdelocalisation. Ajoutezlaméthodededéléguéquiconvient,vousconstaterezquenousrécupérons ainsiunobjetdetype CLHeading dontlapropriété trueheading nousintéresseratout particulièrement. Pourtestercettefonctionnalité,vousdevrezutiliserunvraiterminaliOSplutôtquele simulateur.n essayezpasdetournerlesimac )
Pour orienter la carte avec la direction de l utilisateur, nous appliquerons une transformation affine sur notre objet de type MKMapView (rappelez vous que la propriété transform est animable...). Nous effectuerons une rotation du composant graphique qui donnera l impression que son contenu s oriente avec l utilisateur. Recherchez dans la documentation la propriété transform. Trouvé? C est une propriété de la classe UIView (dont hérite MKMapView) qui est de type CGAffineTransform. Parcourez la référence pour retrouver les fonctions C qui permettent de définir une transformation affine, vous trouverez notamment la fonction CGAffineTransformMakeRotation Depuis le StoryBoard, redimensionnez la carte de façon à ce qu elle soit trois fois plus grande (en largeur et hauteur) et centrée sur le centre de la vue. Ainsi, lorsque nous appliquerons une rotation, la carte apparaîtra toujours en plein écran. Dans la méthode de délégué locationmanager:didupdateheading:, modifiez la propriété transform de mapview pour effectuer la rotation. Attention, trueheading est en degré, tandis que CGAffineTransformMakeRotation utilise des angles en radians. Comme transform est une propriété animable, vous pouvez ajouter un bloc d animation pour rendre la rotation plus naturelle.