Accueil Nos publications Blog Communiquer facilement avec IOS

Communiquer facilement avec IOS

apple-logoDepuis iOS 3.0, Apple a introduit la possibilité de faire communiquer facilement des appareils via le framework GameKit. Ce framework supporte la recherche, la connexion et la communication avec les appareils iOS autour de vous, que cela soit en bluetooth ou wifi. Son avantage est qu’il masque toute la partie de gestion des connexions et fournit tout ce dont vous avez besoin en une seule classe nommée GKSession.

Avec l’arrivée d’IOS 7.0, un nouveau framework de communication a fait son apparition, Multipeer Connectivity. Semblable en fonctionnalités au GameKit, il est plus proche du système Bonjour, qui permet de découvrir, sans configuration de la part de l’utilisateur, n’importe quel appareil sur un réseau local (Lan, Wifi…) via l’IP et cela dans de nombreux langage, dont Cocoa.

Multipeer Connectivity permet l’échange de données, d’images ou de vidéos avec les appareils environnants, qu’ils soient sur le même Wifi ou connectés en Bluetooth. Si vous recherchez de la communication via internet, je vous recommande l’utilisation du framework GameCenter qui est plus approprié.

 

Framework

 

Multipeer Connectivity s’accompagne de 4 éléments principaux :

  • MCPeerID : Une instance de cette classe représente un appareil.
  • MCAdvertiserAssistant : Cette classe affiche les invitations à l’utilisateur et gère aussi les réponses.
  • MCBrowserViewController : Cette classe affiche la liste des appareils à proximité et permet à l’utilisateur de leur envoyer une invitation.
  • MCSession : Cette classe permet la communication entre les différents appareils connectés. Il est de bonne pratique de créer une classe dédiée à la gestion de la session, qui implémentera le protocole MCSessionDelegate.

Avant d’implémenter les méthodes du protocole, vous devez initialiser les éléments liés au framework. Pour ce faire, j’ai créé les trois méthodes publiques ci-dessous.

 

Initialisation

-(void)setupPeerAndSessionWithDisplayName:(NSString *)displayName
{
    _peerID = [[MCPeerID alloc] initWithDisplayName:displayName];
    _session = [[MCSession alloc] initWithPeer:_peerID];
    _session.delegate = self;
}

On initialise le peerID avec le nom sous lequel notre appareil apparaîtra sur le réseau, puis on crée la session.

 

Recherche

Ecran de recherche des appareils à proximités

 

Le MCBrowserViewController est la vue fournie par Apple pour afficher les appareils à proximité.

Seuls les appareils qui acceptent les invitations seront affichés à l’écran.

 

-(void)setupMCBrowser{
    _browser = [[MCBrowserViewController alloc] initWithServiceType:@"chat-files" session:_session];
}

Le premier paramètre est le nom du service que va rechercher votre contrôleur, si le MCAdvertiserAssistant n’a pas défini le même nom, il ne sera pas remonté.

Le nom du service doit respecter deux règles :

  • Il doit faire entre 1 et 15 caractères
  • Il ne doit contenir que des lettres minuscules ASCII, des nombres, et des traits d’union.

Il est tout à fait possible de créer une interface personnalisée et d’utiliser des services comme NSNetService ou l’API Bonjour pour gérer les connections des appareils (voir MCSession).

 

Visibilité

-(void)advertiseSelf:(BOOL)shouldAdvertise{
    if (shouldAdvertise) {
   _advertiser = [[MCAdvertiserAssistant alloc]
                      initWithServiceType:@"chat-files"
                            discoveryInfo:nil
                                  session:_session];
        [_advertiser start];
    }
    else{
        [_advertiser stop];
        _advertiser = nil;
    }
}

Le MCAdvertiserAssistant permet de spécifier si vous acceptez les invitations.

 

Connexion

Une session peux avoir trois statuts :

  • MCSessionStateNotConnected : l’appareil pouvait être connecté mais ne l’est plus.
  • MCSessionStateConnecting : l’appareil attend la réponse à la demande de connexion.
  • MCSessionStateConnected : la connexion a été établie avec succès.

Il est de bonne pratique de toujours implémenter la gestion du statut de la session car celle-ci peux être coupée à tout moment pour un des appareils.

Vous êtes fin prêts pour établir une connexion entre deux appareils. Un clic sur le nom d’un appareil trouvé va envoyer une demande de connexion que les utilisateurs peuvent accepter ou refuser.

Demande de connexion

 

Communication

 

Maintenant que nos appareils sont connectés, il faut gérer leur communication.

Etat de la session

-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state

Celle-ci est appelée lors d’une nouvelle connexion et fournit le peerID de l’appareil qui rejoint la session, ainsi que son état. Il vous suffit ensuite d’envoyer ces informations à la classe concernée (via délégation, notification…), pour qu’elle réagisse en conséquence (ex : ajouter le nom du peer à la liste des appareils connectés).

Echange de données

Semblable au protocole TCP/UDP, MPC vous permet d’envoyer ou de recevoir des données, avec ou sans gestion des paquets, qui peuvent être séparées en trois grandes familles : les données brutes, les fichiers et les flux.

Données brutes

Pour envoyer des informations (texte, objet…), le framework nous oblige à les encapsuler dans une classe NSData, souvent utilisée pour la sauvegarde car n’a pas de taille définie, évitant ainsi la multiplicité des méthodes en fonction du type d’objet envoyé.

NSString *message = @"Hello, World!";
NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;

if (![self.session sendData:data
                    toPeers: session.connectedPeers
                   withMode:MCSessionSendDataReliable
                      error:&error])
{
    NSLog(@"[Error] %@", error);
}

Une autre approche serait d’utiliser la classe NSKeyedArchiver mais il faudra alors que votre objet réponde au protocole NSSecureCoding pour se protéger des attaques de substitution.

id <NSSecureCoding> object = // ...;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];

Quant à la réception, c’est notre gestionnaire de la session qui va s’en occuper.

-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID

Comme pour la gestion de l’état de la session, une fois les données extraites de l’objet NSData, elles seront envoyées à classe concernée (ex : mettre à jour la vue contenant une discussion en cours).

 

Fichier

- (NSProgress *)sendResourceAtURL:(NSURL *)resourceURL
                          withName:(NSString *)resourceName
                            toPeer:(MCPeerID *)peerID
             withCompletionHandler:(void (^)(NSError *error))completionHandler

Avant d’envoyer un fichier, il est important de noter le type de retour cette méthode. La classe NSProgress est une nouvelle classe qui permet de suivre le transfert d’un fichier, via la propriété fractionCompleted, permettant de mettre à jour la vue. Evidemment, ce n’est pas si simple, car si l’on appelle cette méthode sur le thread principal, notre vue va rester figée jusqu’à la fin de l’envoi. Pour contourner ce problème, vous allez devoir utiliser dispatch_async pour exécuter le traitement sur un autre thread.

dispatch_async(dispatch_get_main_queue(), ^{
    NSProgress *progress = [self.session
           sendResourceAtURL:resourceURL
                    withName:modifiedName
                      toPeer:[[session connectedPeers] objectAtIndex:buttonIndex]
       withCompletionHandler:^(NSError *error) {
    if (error) {
        NSLog(@"Error: %@", [error localizedDescription]);
    }
    else{
        NSLog(@"Success");
    }
   //On lie notre classe à l’objet progress pour observer tout changement de valeur
    [progress addObserver:self
               forKeyPath:@"fractionCompleted"
                  options:NSKeyValueObservingOptionNew
                  context:nil];
}];

Il ne vous restera plus qu’à implémenter la méthode qui mettra à jour la vue au fur et à mesure de l’envoie du fichier.

Pour la réception, vous allez devoir implémenter deux méthodes.

-(void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress{
     
     //Envoi des données à la classe concernée
     …
 
     //Mise à jour de la vue    
     dispatch_async(dispatch_get_main_queue(), ^{
             [progress addObserver:self
                        forKeyPath:@"fractionCompleted"
                           options:NSKeyValueObservingOptionNew
                           context:nil];
     });
 }

Comme son nom l’indique, cette méthode permet de suivre l’avancée du téléchargement d’un fichier.
Comme pour l’envoi, nous retrouvons dispatch_async pour permettre à l’utilisateur de suivre le téléchargement du fichier.

-(void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error

Une fois le téléchargement terminé, le framework vous fournira l’emplacement du fichier ainsi que l’appareil qui l’a envoyé.

NSURL *destinationURL = [NSURL fileURLWithPath:destinationPath];
 
  NSFileManager *fileManager = [NSFileManager defaultManager];
 NSError *error;
 [fileManager copyItemAtURL:localURL toURL:destinationURL error:&error];

Ci-dessus, un extrait de la démo, où la classe NSFileManager est utilisée pour recopier le fichier téléchargé dans un dossier spécifique de l’application.

 

Flux

-(void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID

Utilisé pour traiter un flux constant de données (Youtube, Twitch…). Son fonctionnement étant plus complexe que les deux premiers, nous ne le traiterons pas dans ce tutoriel.

 

Complications

 

L’un des problèmes majeurs du framework est la gestion des connexions. Une des fonctionnalités de celui-ci est de lever des événements en fonction des statuts des appareils environnants.

 

Détecter un appareil qui n’est pas à proximité

En général, quand un appareil n’est plus joignable, la méthode [MCNearbyServiceBrowserDelegate browser:lostPeer:] est appelé et, bien que celle-ci fonctionne la plupart du temps, dans certain cas, l’UI continuera d’afficher un appareil injoignable et cela même si l’on ré-instancie la classe gérant la recherche (MCBrowserViewController). De ce fait, il est impossible de savoir quand l’appareil est de nouveau joignable.

 

Détecter quand un appareil a perdu la connexion

A chaque changement d’état de la session, la méthode[MCSessionDelegate session:peer:didChangeState:] est appelée sur le délégué, mais il arrive que cette notification ne soit pas immédiate ou n’arrive tout simplement pas. Si vous avez besoin de détecter quand un appareil se déconnecte, une solution est d’implémenter un ping qui enverra, toutes les x secondes, un message pour s’assurer que l’autre appareil est bien connecté.

 

Dernier mot

 

A travers cet article, je voulais vous montrer la simplicité et l’embellissement que cela peut apporter à vos applications. Si vous souhaitez approfondir le sujet, vous pouvez télécharger ma démo sur le Github de SOAT ou explorer la documentation d’Apple sur le sujet. Si vous cherchez un exemple en Swift, je vous recommande l’article de JP Simard.

Bien qu’aucune nouveauté ne soit apparue dans iOS 9, une amélioration du système de gestion des connections, en reconnectant automatiquement les appareils, serait très appréciable, mais même en l’état actuel, Multipeer Connectivity peut vraiment transformer votre application, alors faites en quelque chose d’exceptionnel.