Category Archives: iOS

ITMS-90668 : The executable file contains incomplete bitcode

Comme toute personne qui développe des applications mobiles iOS avec Xamarin, vous utiliserez certainement à un moment donné des SDKs tierces (réseaux sociaux, régies publicitaires, etc…).

Une fois votre application développée, viendra le moment de la soumettre sur l’AppStore et dans la plupart des cas cela fonctionnera sans aucun problème. Mais il arrivera parfois que des soucis surviennent.

C’est ce qui m’est arrivé il y a 15 jours environ pendant la phase de soumission sur l’AppStore, quand j’ai reçu un message d’erreur comme celui visible ci-dessous (cette capture ne correspond pas à mon application mais la source d’erreur est la même).

Ce message indique qu’un SDK utilisé dans l’application pose un souci car il contient un “bitcode incomplet” alors que très probablement d’autres SDKs dans la même application utilisent pour leur part un bitcode complet. Apple refuse apparemment d’accepter les applications qui mixent les deux.

Après de multiples recherches sur Internet qui traitent dans la majorité des cas de la résolution de cette erreur pour les gens travaillant sur Xcode, je suis tombé sur cette page sur l’outil de suivi de bugs de Xamarin qui fait état d’un problème connu.

La solution de contournement (workaround / fix) évoqué dans le ticket fonctionne parfaitement.

Pour cela, il suffit d’éditer le fichier .csproj de votre application Xamarin.iOS et d’ajouter les lignes suivantes à la fin, juste avec la balise </project> :

<Target Name="BeforeCodesign">
  <Exec Command="$(_SdkDevPath)\Toolchains\XcodeDefault.xctoolchain\usr\bin\bitcode_strip -r %(_Frameworks.FullPath) -o %(_Frameworks.FullPath)" />
</Target>

Ces quelques lignes permettent de supprimer tout le bitcode inutiles des SDKs embarqués dans votre application au moment où celle-ci est compilée et juste avant d’appliquer la signature du package.

Une fois ces quelques lignes ajoutées et l’application recompilée, la soumission sur l’AppStore ne devrait plus poser aucun souci.

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9814)

En informatique, les problèmes les plus simples sont parfois les plus bizarres à comprendre et c’est ce qui m’est arrivé il y a 2 jours sur un prototype Xamarin.iOS sur laquelle je travaille.

L’application est on ne peut plus classique puisqu’elle fait simplement des appels à des webservices pour afficher des données sous forme d’une liste.

Alors que tout fonctionne correctement au début, en passant les webservices de HTTP à HTTPS, l’erreur, dont la description est très parlante (NSURLSession/NSURLConnection HTTP load failed kCFStreamErrorDomainSSL, -9814), se produit de manière subite.

La première réaction a été de se dire que j’ai oublié de configurer ATS (App Transport Security) dans le fichier Info.plist de l’application. Après vérification, ce n’est pas le cas puisque la configuration autorise tous les appels comme indiqué ci-dessous.

<key>NSAppTransportSecurity</key>
  <dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
  </dict>
</key>

Après recherche dans la documentation officielle Apple, le code 9814 signifie que le certificat SSL est expiré mais ce n’est pas le cas puisque celui-ci expire en 2019.

D’où peut donc bien venir cette erreur dans ce cas ?

Après plus d’une heure à tourner en rond, en cherchant sur StackOverflow et toutes les sources classiques d’aide pour un développeur, une illumination m’est apparue : “Vérifions quelle date est utilisée par mon périphérique actuellement”.

BINGO !!! Le périphérique qui n’a apparemment jamais réussi à se synchroniser avec une source de temps (ou défini manuellement dans les paramètres du système), utilise une date en Janvier 1970.

L’erreur ne signifie pas dans ce cas que le certificat est réellement expiré, mais qu’il n’a pas réussi à être validé dans la séquence normale de validation des communications HTTPS.

Je partage donc cette petite mésaventure qui vous évitera de perdre du temps, simplement parce que le périphérique utilisé n’était pas à l’heure. Après vérification, la même erreur s’applique sur tvOS si jamais vous travaillez sur cette plateforme.

Reconnaissance vocale et analyse de texte dans vos applications Xamarin.iOS

Bien qu’existante depuis très longtemps dans l’univers informatique, la reconnaissance vocale est une fonctionnalité encore peu utilisée au sein des applications (aussi bien sur mobile que sur desktop).

Depuis l’annonce de Siri par Apple en 2011, les assistants personnels ont le vent en poupe et une nouvelle étape a été franchie sur iOS 10 lors de la WWDC 2016 puisque le moteur de reconnaissance vocal utilisé par Siri est désormais accessible aux développeurs.

Voyons comment utiliser celui-ci pour intégrer la reconnaissance vocale au sein d’une application Xamarin.iOS, et comment le coupler à des APIs plus anciennes mais relativement méconnues, pour faire de l’analyse de texte.

Speech Recognition API au service des développeurs

Comme nous l’évoquions précédemment, Apple a rendu disponible dans iOS 10 le moteur qui propulse Siri pour la reconnaissance vocale. Pour cela, une nouvelle classe est à la disposition des développeurs : SFSpeechRecognizer.

Ce qu’il est important de noter pour utiliser cette classe, c’est qu’une connexion réseau est nécessaire car tout ce qui sera enregistré via le microphone, sera envoyé à Apple pour que les serveurs qui propulsent Siri, traitent et analysent les informations avant de vous renvoyer la transcription du texte.

Cette phase de communication avec les serveurs Apple a lieu tout au long du processus de reconnaissance vocale (vous récupérez la transcription au fil de l’eau), ce qui fait que si vous utilisez la fonctionnalité trop souvent, cela a tendance à consommer beaucoup de données (sur votre forfait 3G/4G) et d’énergie sur la batterie de votre téléphone/tablette.

C’est pourquoi Apple impose certaines limitations (ex: 1mn maximum pour chaque enregistrement audio) et a également mis en place des quotas d’utilisation permettant à une application d’effectuer un nombre limité de requêtes par jour (un message d’erreur est renvoyé si le quota est dépassé).

Comme pour tout ce qui touche à la vie privée chez Apple, avant de pouvoir utiliser cette API, il est nécessaire de demander l’autorisation à l’utilisateur et de réagir en conséquence selon la réponse qu’il donnera :

SFSpeechRecognizer.RequestAuthorization((SFSpeechRecognizerAuthorizationStatus status) =>
{
  switch (status)
  {
    case SFSpeechRecognizerAuthorizationStatus.Authorized:
      break;
    case SFSpeechRecognizerAuthorizationStatus.Denied:
      break;
    case SFSpeechRecognizerAuthorizationStatus.NotDetermined:
      break;
    case SFSpeechRecognizerAuthorizationStatus.Restricted:
      break;
  }
});

Pour que tout ceci fonctionne sans soucis, il est également nécessaire d’ajouter 2 lignes dans le fichier Info.plist, afin d’indiquer à l’application que vous avez besoin d’utiliser la reconnaissance vocale et le microphone.

Speech Recognition - Info.plist

Maintenant que les bases sont posées pour pouvoir utiliser les composants nécessaires, il ne reste plus qu’à écrire le code. Celui-ci repose sur 3 classes importantes :

  • AVAudioEngine ==> Le moteur audio permettant d’enregistrer ce que l’utilisateur dira via le microphone de son téléphone ou de sa tablette
  • SFSpeechRecognizer ==> Le moteur de reconnaissance vocal qui communiquera avec les serveurs Apple
  • SFSpeechAudioBufferRecognitionRequest ==> Le buffer audio qui servira à stocker les données renvoyées par le moteur audio avant d’être envoyées chez Apple pour analyse

Un exemple de code étant toujours plus parlant qu’un long discours, voici ci-dessous le code associé à notre application de démonstration, qui initie l’enregistrement quand l’utilisateur tape sur un bouton, et met fin à l’enregistrement quand l’utilisateur relâche ce même bouton.

private AVAudioEngine AudioEngine;
private SFSpeechRecognizer SpeechRecognizer;
private SFSpeechAudioBufferRecognitionRequest LiveSpeechRequest;
private SFSpeechRecognitionTask RecognitionTask;
private string sentence;

partial void StartSpeechRecognition(UIButton sender)
{
  SpeechRecognizer = new SFSpeechRecognizer(NSLocale.FromLocaleIdentifier("fr"));
  if (!SpeechRecognizer.Available)
    return;

  AudioEngine = new AVAudioEngine();
  if (AudioEngine.InputNode == null)
    return;

  NSError audioError;
  LiveSpeechRequest = new SFSpeechAudioBufferRecognitionRequest();

  var recordingFormat = AudioEngine.InputNode.GetBusOutputFormat(0);
  AudioEngine.InputNode.InstallTapOnBus(0, 1024, recordingFormat, (AVAudioPcmBuffer buffer, AVAudioTime when) => LiveSpeechRequest.Append(buffer));
  AudioEngine.Prepare();
  AudioEngine.StartAndReturnError(out audioError);
  if (audioError != null)
    return;

  RecognitionTask = SpeechRecognizer.GetRecognitionTask(LiveSpeechRequest, (SFSpeechRecognitionResult result, NSError taskError) =>
  {
    if (taskError != null)
      return;

    if (!result.Final)
    {
      SpeechLabel.Text = result.BestTranscription.FormattedString;
      return;
    }
 
    sentence = result.BestTranscription.FormattedString;
    ParseSpeechRecognition();
  });
}

partial void StopSpeechRecognition(UIButton sender)
{
  AudioEngine?.Stop();
  AudioEngine?.InputNode?.RemoveTapOnBus(0);
  LiveSpeechRequest?.EndAudio();
}

Outre l’initialisation du moteur audio via la définition de paramètres sur la source d’enregistrement (AudioEngine.InputNode), la partie importante repose sur la fonction de “callback” passée à la méthode GetRecognitionTask, qui permet de récupérer la transcription du texte (result.BestTranscription.FormattedString) au fil de l’eau, et de savoir si la reconnaissance est terminée ou non (result.Final).

Une fois tous les éléments assemblés et en réalisant un peu de cosmétique autour de l’interface graphique, cela donne le résultat visible dans la vidéo ci-dessous :

Comme on peut le voir dans cette vidéo, avec juste quelques lignes de code, il est possible d’intégrer la reconnaissance vocale dans une application. Mais hormis si l’on souhaite juste permettre à l’utilisateur de remplir des zones de texte sans rien saisir au clavier, il serait plus intéressant de pouvoir analyser ce texte.

NSLinguisticTagger : Une classe méconnue mais très pratique

C’est à ce moment qu’une classe relativement méconnue surgit, bien qu’elle existe dans iOS depuis la version 5.0. Cette classe porte un nom relativement explicite : NSLinguisticTagger.

Celle classe est capable, à partir d’un texte plus ou moins long, de “tagger” celui-ci, c’est-à-dire d’identifier tous ses composants (noms, pronoms, verbes, adverbes, déterminants, etc…) mais également d’identifier certains éléments marquants (noms de lieux, noms de sociétés…). Malheureusement cette dernière possibilité ne fonctionne qu’en Anglais.

Différentes langues sont supportées par NSLinguisticTagger (Anglais, Français, Allemand…) afin de couvrir les langues les plus fréquemment rencontrées sur iOS.

Comment fonctionne tout ceci ? C’est relativement simple comme on peut le voir dans l’exemple de code ci-dessous :

private void ParseSpeechRecognition()
{
  var options = NSLinguisticTaggerOptions.OmitWhitespace | NSLinguisticTaggerOptions.OmitPunctuation | NSLinguisticTaggerOptions.JoinNames;
  var schemes = NSLinguisticTagger.GetAvailableTagSchemesForLanguage("fr");
  var tagger = new NSLinguisticTagger(schemes, options);
  tagger.AnalysisString = sentence;
  tagger.EnumerateTagsInRange(new NSRange(0, sentence.Length), NSLinguisticTag.SchemeLexicalClass, options, HandleNSLingusticEnumerator);
}

private void HandleNSLingusticEnumerator(NSString tag, NSRange tokenRange, NSRange sentenceRange, ref bool stop)
{
  var token = sentence.Substring((int)tokenRange.Location, (int)tokenRange.Length);
  Console.WriteLine($"{token} = {tag}");
}

A partir d’une liste d’options (ignorer les espaces, ignorer la ponctuation, etc…) et d’une liste de types d’éléments analysables pour une langue (appelées “schemes”), on énumère les “tags” contenus dans le texte à analyser (AnalysisString).

La méthode permettant d’énumérer les tags du texte, prend en paramètre une fonction de “callback” qui sera appelée pour chaque tag identifié.

Si on exécute le code précédent avec la phrase utilisée dans la vidéo de notre application de démonstration, nous obtenons le résultat ci-dessus dans la fenêtre de sortie de Xamarin Studio.

2016-11-27 09:02:57.714 SpeechDemo[919:415497] C' = Pronoun
2016-11-27 09:02:57.714 SpeechDemo[919:415497] est = Verb
2016-11-27 09:02:57.714 SpeechDemo[919:415497] bientôt = Adverb
2016-11-27 09:02:57.715 SpeechDemo[919:415497] Noël = Noun
2016-11-27 09:02:57.715 SpeechDemo[919:415497] j' = Pronoun
2016-11-27 09:02:57.715 SpeechDemo[919:415497] espère = Verb
2016-11-27 09:02:57.716 SpeechDemo[919:415497] que = Conjunction
2016-11-27 09:02:57.716 SpeechDemo[919:415497] vous = Pronoun
2016-11-27 09:02:57.716 SpeechDemo[919:415497] passerez = Verb
2016-11-27 09:02:57.716 SpeechDemo[919:415497] tous = Adjective
2016-11-27 09:02:57.717 SpeechDemo[919:415497] de = Preposition
2016-11-27 09:02:57.717 SpeechDemo[919:415497] bonnes = Adjective
2016-11-27 09:02:57.717 SpeechDemo[919:415497] fêtes = Noun

Comme on le voit ci-dessus, à partir des informations renvoyées par NSLinguisticTagger, il est possible d’envisager les traitements nécessaires à la logique propre à une application (ex: extraire les noms et rechercher des valeurs particulières pour filtrer des résultats).

Pour aller encore plus loin…

Comme nous l’avons vu dans cet article, en couplant la reconnaissance vocale, désormais disponible dans iOS 10, avec un outil de taggage comme NSLinguisticTagger, de nouveaux horizons s’ouvrent pour intégrer des fonctionnalités inédites au sein des applications mobiles.

Toutefois si vous voulez aller encore plus, il existe d’autres éléments qu’il est toujours bon de connaitre, à commencer par la catégorisation de contenus.

Si vous réalisez des applications pour OSX, Apple a mis à disposition un framework appelé Latent Semantic Mapping (LSM) qui permet de catégoriser un contenu (ex: une page web, un fichier…) à partir d’une base d’autres contenus. Une session dédiée à ce framework avait été présentée lors de la WWDC 2011 si vous souhaitez en savoir plus à ce sujet.

Si vous voulez faire une analyse plus poussée du texte renvoyé par la reconnaissance vocale (ex: extraire des entités, identifier le sujet de fond d’un document, etc…) il existe différentes APIs basées sur le Machine Learning qui satisferont tous vos désirs.

On pourra citer comme exemple un projet sur lequel mise beaucoup Microsoft, à savoir Cognitive Services et plus particulièrement la brique Language Understanding Intelligent Service (LUIS). Des alternatives telles que TextRazor méritent également le coup d’oeil.

Editer un XIB avec Xcode 8 peut avoir des conséquences inattendues sur le rendu de votre application Xamarin

Apple a rendu disponible la version finale de Xcode 8 courant Septembre et comme beaucoup de développeurs, vous avez probablement effectué la mise à jour si vous voulez pouvoir tirer parti des nouveautés d’iOS 10.

Ayant déjà essuyer les plâtres de ce type de mise à jour par le passé, notamment lors des différentes montées de version de Swift (de 1.0 à 3.0), je laisse désormais passer un peu de temps pour voir si d’autres personnes rencontrent des soucis ou non, même si j’avais déjà testé de manière superficielle cette version sur une 2ème machine de développement à sa sortie.

J’ai donc appliqué le même principe avec cette dernière mise à jour, et j’ai effectué des tests un plus poussés cette semaine pour voir si une application Xamarin.iOS sur laquelle travaille mon équipe, fonctionnait toujours correctement une fois la mise à jour effectuée.

Une interface graphique relativement simple basée sur des XIB

Quand on réalise une application iOS, on peut réaliser l’interface graphique sous 3 formes, en pouvant combiner celles-ci si nécessaire :

  • Par code => Il faut être motivé si les écrans sont complexes
  • A base de XIB => La solution historique utilisée sur iOS depuis ses débuts
  • A base de storyboards => La solution désormais préconisée par Apple

Nous ne débattrons pas ici des avantages et inconvénients de chaque solution et pour l’application sur laquelle nous travaillons, nous avons préféré retenir les XIB avec également la solution par code sur certains composants.

Ci-dessous, vous trouverez l’exemple d’un écran de l’application réalisé par XIB sous Xcode 7, avec l’utilisation de contraintes pour gérer facilement les différentes tailles/résolutions de périphériques. Cet écran contient également un composant entièrement réalisé par code (le calendrier sur l’onglet “Mois”).

Xcode8 - Application avant édition #1  Xcode8 - Application avant édition #2

Ouverture du XIB sous Xcode 8 sans apporter aucune modification…

Après avoir mis à jour les différents outils (Xamarin Studio, Xcode, etc…) sur une machine de développement, j’ai tout simplement ouvert le XIB de cet écran depuis Xcode 8 afin de pouvoir tirer parti des nouveautés de l’environnement Apple (ex: la nouvelle prévisualisation sur différents périphériques).

A l’ouverture du XIB, Xcode 8 fait un certain nombre de modifications dont vous n’avez pas nécessairement conscience, et enregistre automatiquement celles-ci. Le mécanisme de synchronisation entre Xamarin Studio et Xcode fait en sorte que ces modifications soient immédiatement répercutées côté Xamarin à la prochaine compilation de l’application.

… et là c’est le drame une fois l’application compilée et exécutée

Après être retourné sous Xamarin Studio et avoir compiler et exécuter l’application, je retourne sur l’écran en question et là surprise, celui-ci ne fonctionne plus comme auparavant.

Les différences sont subtiles (disparition du liseré rouge sous l’onglet courant, disparition de certaines lignes dans le contrôle calendrier…). Vous pouvez voir ci-dessous le résultat :

Xcode8 - Application après édition #1 Xcode8 - Application après édition #2

Mais que se passe-t-il vu que nous avons juste ouvert le XIB ?

Comme je le disais précédemment, lors de l’ouverture du XIB, Xcode 8 fait quelques modifications sans vous prévenir dans la structure du fichier, dont vous pouvez voir un extrait ci-dessous grâce au client Git que nous utilisons pour nos projets (Atlasssian SourceTree).

Xcode8 - Différences dans le fichier XIB

Comme on peut le voir, certaines lignes ont été ajoutées et/ou modifiées (en vert) et d’autres lignes ont purement et simplement été supprimées (en rouge).

Parmi les lignes supprimées, on retrouve tout ce qui concerne la définition du positionnement par défaut des éléments graphiques dans la vue (les lignes commençant par <rect key=”frame” x=”…” y=”…” width=”…” height=”…” />.

Vous vous demandez peut être pourquoi un tel impact sur le rendu de l’écran ?

Et bien parce que dans celui-ci, certains éléments sont construits par code (le liseré rouge ou le contrôle calendrier) avec du code ressemblant à quelque chose comme ceci :

public override void ViewDidLoad()
{
   base.ViewDidLoad();
   ...

   _selectedBottomLayer = new CALayer();
   _selectedBottomLayer.Frame = new CGRect(0, DayButton.Bounds.Size.Height - SELECTED_LAYER_THICKNESS, DayButton.Bounds.Width, SELECTED_LAYER_THICKNESS);
   View.Layer.AddSublayer(_selectedBottomLayer);

   ...
}

Rien que des choses assez classiques pour les personnes qui font de l’iOS, mais qui ne fonctionnent plus comme auparavant à cause de la modification opérée par Xcode 8.

La suppression de la position par défaut a un impact dans le cycle de vie de construction d’une vue

Le fait que Xcode 8 aie supprimé le positionnement par défaut des éléments n’a pas d’impact sur les éléments positionnés par contraintes, mais pour les éléments positionnés par code et sans contraintes (les contraintes ne peuvent pas toujours être utilisées), cette modification a un impact très important.

Dans l’exemple précédent, le positionnement du liseré rouge est calculé dans la méthode ViewDidLoad. Le problème étant qu’au moment où cette méthode est exécutée dans le cycle de vie iOS, les contraintes appliquées sur les composants graphiques n’ont pas encore été calculées.

Là où au préalable la bouton sur lequel étaient basés les calculs avait un cadre défini comme ceci (x=0, y=0, width=106.5, height=45), il n’a désormais plus de positionnement par défaut ce qui fait qu’au moment ou la méthode ViewDidLoad s’exécute, le bouton a les caractéristiques suivantes (x=0, y=0, width=1000, height=1000).

Donc le liseré rouge est bien construit mais avec des coordonnées totalement farfelues qui le positionnent en dehors de la zone visible de l’écran. Le problème est le même pour les lignes qui ont disparues du contrôle calendrier.

Quelle est la solution pour résoudre les soucis dans ce cas ?

La solution est relativement simplement à mettre en oeuvre mais peut nécessiter de revoir complètement le cycle de vie qui a été utilisé dans les différents écrans.

Puisque le calcul de positionnement ne peut être effectué correctement au moment du ViewDidLoad, il suffit de le faire à un moment plus opportun.

Il n’y a pas une réponse qui pourra répondre à tous les cas possibles que vous pourriez rencontrer mais iOS propose différentes méthodes à surcharger dans lesquelles vous pouvez effectuer les traitements nécessaires à vos besoins.

Parmi les méthodes classiques que l’on a l’habitude d’utiliser pour effectuer les traitements nécessaires à des calculs de positionnement, on retrouve celles-ci (liste non exhaustive) :

public override void UpdateViewConstraints()
{
}

public override void ViewDidAppear(bool animated)
{
}

public override void ViewDidLayoutSubviews()
{
}

public override void ViewWillAppear(bool animated)
{
}

public override void ViewWillLayoutSubviews()
{
}

En conclusion, même si dans la plupart des cas la montée de version de Xcode n’aura pas d’impact sur vos applications, il peut arriver que dans certains cas cela ne soit pas si transparent que cela.

Que Xcode 8 supprime des données du XIB est une chose qui parait assez logique, certainement pour privilégier le système de contraintes créé par Apple, mais le fait que cela change le comportement et l’affichage d’une application qui fonctionnait sans soucis jusque là, est plus dérangeant sur le fond.

On ne répètera jamais assez qu’avant de faire une montée de version quelle qu’elle soit (outils de développement, packages NuGet…), la mise en place de tests approfondis (unitaires, graphiques, etc…) pour vérifier tous les impacts est un élément crucial pour la qualité de vos applications.

How to remove the IOHIDLibFactory error when you start your Xamarin.iOS application in the simulator

When you will develop Xamarin.iOS applications, you will use the iOS simulator to test your application without having to use a real device like an iPhone or an iPad.

In Xamarin Studio (like in Visual Studio), the output window gives you some useful information to help you to debug your application (e.g. when you use Console.WriteLine(…) to print values).

When you start your application in the iOS simulator, it might happen that you see a lot of errors like this :

Error loading /System/Library/Extensions/IOHIDFamily.kext/Contents/PlugIns/
IOHIDLib.plugin/Contents/MacOS/IOHIDLib: dlopen(/System/Library/Extensions/
IOHIDFamily.kext/Contents/PlugIns/IOHIDLib.plugin/Contents/MacOS/IOHIDLib, 262):
no suitable image found. Did find: /System/Library/Extensions/IOHIDFamily.kext/Contents/
PlugIns/IOHIDLib.plugin/Contents/MacOS/IOHIDLib: mach-o, but not built for iOS simulator

Or also :

Cannot find function pointer IOHIDLibFactory for factory 13AA9C44-6F1B-11D4-907C-0005028F18D5 in CFBundle/CFPlugIn 0x7be04110 </System/Library/Extensions/
IOHIDFamily.kext/Contents/PlugIns/IOHIDLib.plugin> (bundle, not loaded)

It’s not a problem for your application and has no impact on it, but it’s often annoying to have a lot of errors in the output window, each time you start your application.

If you want to remove these errors, you just have to edit your iOS build settings, and change the linker behavior value from “Don’t link” to “Link Frameworks SDKs Only“.

Xamarin Studio - iOS Build Settings

Note that activating this option will increase the compile time of your application because the linker will have more work to do to generate your package.

Introducing the new HttpClient implementation in Xamarin.iOS

In the upcoming days/weeks, Xamarin will release a whole set of new versions for its products to develop cross-platform mobile applications.

One of the most important (from my perspective) is Xamarin Studio 6.0 which will include lots of new features. If you want more information about those features, you can find a complete list on this page.

Today, I would like to talk about one of them which allows you to choose which implementation you want to use for network communications for your application iOS.

When you want to write cross-platform mobile applications, you avoid to use platform-specific APIs such as NSURLSession in iOS. Instead, you will prefer to use APIs that are available on all platforms such as the HttpClient class available in the .NET framework.

While this solution has many benefits for reusing your code on all platforms, the performance is worse than using specific API to the platform.

Fortunately, Xamarin Studio 6.0 (available on the Beta channel at this time) will introduce an option to use the underlying native APIs (NSURLSession, CFNetwork…) above HttpClient.

To activate this option, you just have to edit your Xamarin.iOS project settings and to go to the iOS Build section. On the screenshot below, you can see that 2 new options are available :

  • HttpClient Implementation
  • SSL / TLS Implementation

By default, your project will use the .NET implementation but you can switch to native APIs.

Xamarin Studio 6.0 - Choose your HttpClient implementation

There are many choices available and if you want more details, leave your cursor on the “blue I” on the right side of the selection lists. Below, the details about choices available for the HttpClient implementation.

Xamarin Studio 6.0 - HttpClient implementation tooltip

As mentioned in this tooltip, the usage of native APIs will result of better performance, support for latest standards (e.g. TLS 1.2) and smaller executable size.

An SSL error has occurred when trying to use the Outlook REST API on iOS 9 or OSX 10.11 (El Capitan)

In a previous article, we talked about an issue when you try to stream an Office 365 Videos from Azure Media Services in your application running on iOS 9.

It seems that Microsoft is actually doing some modifications on the infrastructure of the Office 365 platform, especially around the SSL certificates, because today I’m facing the same error when using the Outlook REST API.

I’m trying to call the following URL which should return the last 10 messages stored in the inbox  of the authenticated user :

https://outlook.office365.com/api/v2.0/Me/MailFolders('Inbox')/Messages

But I get the following error message, which indicates there’s an issue with SSL certificates :

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made."
UserInfo={NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, NSUnderlyingError=0x7d96cd90 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9802, _kCFStreamErrorCodeKey=-9802, _kCFStreamErrorDomainKey=3, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x7afb9d50>, kCFStreamPropertySSLPeerCertificates=<CFArray 0x7ae63490 [0xa97098]>{type = immutable, count = 2, values = (
 0 : <cert(0x7afb8570) s: outlook.com i: Microsoft IT SSL SHA1>
 1 : <cert(0x7afb96d0) s: Microsoft IT SSL SHA1 i: Baltimore CyberTrust Root>
)}}}, _kCFStreamErrorCodeKey=-9802, NSErrorFailingURLStringKey=https://outlook.office365.com/api/v2.0/Me/MailFolders('Inbox')/Messages, NSErrorPeerCertificateChainKey=<CFArray 0x7ae63490 [0xa97098]>{type = immutable, count = 2, values = (
 0 : <cert(0x7afb8570) s: outlook.com i: Microsoft IT SSL SHA1>
 1 : <cert(0x7afb96d0) s: Microsoft IT SSL SHA1 i: Baltimore CyberTrust Root>
)}, NSErrorClientCertificateStateKey=0, NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x7afb9d50>, NSErrorFailingURLKey=https://outlook.office365.com/api/v2.0/Me/MailFolders('Inbox')/Messages}

The cause of this error is still the same as explained in our previous article : App Transport Security (ATS).

To solve the issue, you need to add some lines in your Info.plist to configure ATS to ignore the SSL error.

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains<key>
  <dict>
    <key>outlook.office365.com</key>
    <dict>
      <key>NSExceptionRequiresForwardSecrecy</key>
      <false/>
    </dict>
  </dict>
</dict>

You can also edit this file using the editor in Xcode rather than manually edit the XML file :

Office365 Outlook REST API - App Transport Security

NSURLSession/NSURLConnection HTTP load failed when trying to stream an Office 365 Video on iOS 9

Yesterday, I was playing with Office 365 Video and I wanted to be able to play/stream videos from Office 365 into an iOS app.

The first step to be able to read a video was to retrieve the streaming URL of that video in Azure Media Services (which is used behind Office 365 to offer streaming capabilities).

It’s pretty simple using the Office 365 REST API :

https://tenant.sharepoint.com/portals/hub/_api/VideoService/Channels(guid'01234567-abcd-cded-1234-1234567890ab')/Videos(guid'01234567-abcd-cded-1234-1234567890ab')/GetPlaybackUrl(0)

When you execute that request, you retrieve the URL to play/stream the video from Azure Media Services in HLS format (HTTP Live Streaming). This URL should look something like :

https://cvprdb302v.cloudvideo.azure.net/api/ManifestProxy?playbackUrl=https://cdn-cvprdb302m01.streaming.mediaservices.windows.net/.../Manifest(format=m3u8-aapl)&token=...

To play/stream the video into an iOS app, you can use the built-in video player (AVPlayer) which is compatible with HLS format. To do that, you just have to write few lines of code :

let URL = NSURL(string: "...")  // Put here the URL retrieved from GetPlaybackUrl
let playerViewController = AVPlayerViewController()
playerViewController.player = AVPlayer(URL: URL!)
presentViewController(playerViewController, animated: true, completion: nil)
playerViewController.player?.play()

If you run this code on iOS 8, there’s no problem and everything should work fine. But if you run the same code on iOS 9, it should throw an exception like that :

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

You have to wonder why the same code don’t work on the last version of iOS. The answer is quite simple : App Transport Security (ATS).

In iOS 9, there are new security mechanisms that have been implemented to ensure that, when an app uses network communications, the traffic is encrypted to ensure confidentiality.

But if you look the URL of the video, it uses HTTPS so the traffic is encrypted. Then why an exception was thrown ? It seems that the ciphers used to encrypt the traffic are considered as unsafe by iOS 9.

What to do to be able to play/stream the video in that case ? It’s possible to configure how ATS works by modifying the info.plist file of your app.

There are many ways to do that and the most easiest is to add the following key/values :

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

For in-house apps or development purposes, it’s very useful but not secured because ATS is completely disabled. If you read Apple’s documentation, it also could be a reason to reject your app if you want to publish it on the AppStore.

If you want to manage more precisely which URLs can be used without problems with ATS, you have to add exception domains in your info.plist as you can see above :

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains</key>
  <dict>
    <key>mediaservices.windows.net</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSExceptionRequiresForwardSecrecy</key>
      <false/>
    </dict>
    <key>cloudvideo.azure.net</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSExceptionRequiresForwardSecrecy</key>
      <false/>
 </dict>
</dict>

With these settings, we configure ATS to allow more ciphers to encrypt HTTPS traffic, for two domains (and all sub-domains) used by Azure Media Services to stream videos.

If you prefer to use the editor available in Xcode to edit your info.plist, rather than manually editing the XML file, you just have to add key/values like :

Office365 Video - App Transport Security

Now if we execute our app, we are able to play/stream the video without any exceptions. 😉

Use ADAL for iOS/OSX with O365 Discovery REST API

You want to access your Office 365 data from an iOS / OSX application and don’t know how to start ? You are in the right place 😉

In this article, we will see the first step to write an application connected to Office 365 which consists to discover what services are available for the current user and what are the endpoints URLs for each.

When you want to connect to Office 365 from an application and use the REST API to retrieve data, you need to authenticate against Azure Active Directory (each user accounts are stored in AAD).

To help us to achieve this goal, Microsoft has released a library named ADAL (Active Directory Azure Library) which was written in Objective-C and can be used with iOS and OSX.

You can download ADAL for free on GitHub : https://github.com/AzureAD/azure-activedirectory-library-for-objc

The next step to be able to write an application which operates with Office 365 is to declare it in the Azure directory associated to your Office 365 tenant. If you are a global administrator of your tenant, you can achieve this by using the Azure Management Portal : https://manage.windowsazure.com

On the Azure Management Portal, go to Active Directory section, select the appropriate directory, select applications and create a new native application.

After you fill the requested information (a name and a redirect URL), you have to declare for each service (SharePoint, Exchange, Active Directory…) what permissions are needed by your application as you can see below.

Azure Manage Portal - Application Permissions

We are now ready to write code to connect to Office 365 and to discover what services are available for our user account.

In your application (it works in the same way for iOS and OSX), add the ADAL library in your project as usual when you use a third party library/framework.

    NSString *clientId = @"12345678-abcd-1234-abcd-1234567890ab";
    NSURL *redirectUri = [NSURL URLWithString:@"https://beecomedigitaldemo"];

    ADAuthenticationContext *authenticationContext = [ADAuthenticationContext authenticationContextWithAuthority:@"https://login.windows.net/common" error:nil];
    [authenticationContext acquireTokenWithResource:@"https://api.office.com/discovery/" clientId:clientId redirectUri:redirectUri completionBlock:^(ADAuthenticationResult *result) {
        if (result.status == AD_SUCCEEDED) {
            // Authentication succeeded
        }
        else if (result.status == AD_USER_CANCELLED) {
            // Authentication cancelled by the user
        }
        else {
            // Authentication failed
        }
    }];

To authenticate against Azure Active Directory, we have to use the ADAuthenticationContext class available in ADAL.

To instanciate a context, you need to indicate what is the authentication authority you will use. The default authority to logon with Azure Active Directory is https://login.windows.net/common but if it’s applicable for your company, you can specify another authority.

After the instanciation of the context, you have to acquire an authentication token for the resource which you will request later.

In our case, we want to discover services offered by Office 365. The resource associated is https://api.office.com/discovery/ (be careful to not omit the trailing slash character in the URL otherwise it will not work).

When you want to get a token by calling the acquireTokenWithResource method, you have to set a clientId and a redirectUri. These values are those you have declared in the Azure Management Portal.

This method executes asynchronously so when it’s finished, the completionBlock passed in the last parameter is executed. To ensure that the request was successfull, you can test the result status. If everything was good, status will be equal to AD_SUCCEED. Otherwise it will be equal to AD_FAILED or AD_USER_CANCELLED.

Now that we are successfully authenticated and we have a valid authentication token, we can discover what services are available. To do it, we just have to send a request to the discovery REST API endpoint.

// In-house wrapper of NSURLSession to simplify HTTP communications
BCDHttpClient *httpClient = [[BCDHttpClient alloc] init];

NSURL *serviceURL = [NSURL URLWithString:@"https://api.office.com/discovery/v1.0/me/services"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:serviceURL];
[request setValue:[NSString stringWithFormat:@"Bearer %@", result.accessToken] forHTTPHeaderField:@"Authorization"];

[httpClient dataWithRequest:request completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
    if (error == nil) {
        id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        NSLog(@"%@", jsonObject);
    }
}];

As you can see above, we build a NSMutableURLRequest with the URL of the Office 365 discovery endpoint : https://api.office.com/discovery/v1.0/me/services

To call this endpoint and retrieve information associated to our user profile, we need to add an Authorization HTTP header with the following format : “Bearer our_access_token“.

The access token is the one that have been retrieved by ADAL and is accessible in the result parameter of the completion block.

Then we execute the request by using our in-house implementation of a HTTP client (just a lightweight wrapper around NSURLSession). If the request complete successfully, we deserialize the retrieved data (in JSON format) and print them in the console.

You can see below the result printed in our console for all services found for the current authenticated user. In this example, our user have access to SharePoint and Exchange with many different services for each (MyFiles, RootSite, Calendar, Contacts and Mail).

{
    "@odata.context" = "https://api.office.com/discovery/v1.0/me/$metadata#allServices";
    value =     (
                {
            "@odata.editLink" = "services('MyFiles@O365_SHAREPOINT')";
            "@odata.id" = "https://api.office.com/discovery/v1.0/me/services('MyFiles@O365_SHAREPOINT')";
            "@odata.type" = "#Microsoft.DiscoveryServices.ServiceInfo";
            capability = MyFiles;
            entityKey = "MyFiles@O365_SHAREPOINT";
            providerId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
            providerName = Microsoft;
            serviceAccountType = 2;
            serviceApiVersion = "v1.0";
            serviceEndpointUri = "https://beecomedigitaldemo-my.sharepoint.com/_api/v1.0/me";
            serviceId = "O365_SHAREPOINT";
            serviceName = "Office 365 SharePoint";
            serviceResourceId = "https://beecomedigitaldemo-my.sharepoint.com/";
        },
                {
            "@odata.editLink" = "services('RootSite@O365_SHAREPOINT')";
            "@odata.id" = "https://api.office.com/discovery/v1.0/me/services('RootSite@O365_SHAREPOINT')";
            "@odata.type" = "#Microsoft.DiscoveryServices.ServiceInfo";
            capability = RootSite;
            entityKey = "RootSite@O365_SHAREPOINT";
            providerId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
            providerName = Microsoft;
            serviceAccountType = 2;
            serviceApiVersion = "v1.0";
            serviceEndpointUri = "https://beecomedigitaldemo.sharepoint.com/_api";
            serviceId = "O365_SHAREPOINT";
            serviceName = "Office 365 SharePoint";
            serviceResourceId = "https://beecomedigitaldemo.sharepoint.com/";
        },
                {
            "@odata.editLink" = "services('Contacts@O365_EXCHANGE')";
            "@odata.id" = "https://api.office.com/discovery/v1.0/me/services('Contacts@O365_EXCHANGE')";
            "@odata.type" = "#Microsoft.DiscoveryServices.ServiceInfo";
            capability = Contacts;
            entityKey = "Contacts@O365_EXCHANGE";
            providerId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
            providerName = Microsoft;
            serviceAccountType = 2;
            serviceApiVersion = "v1.0";
            serviceEndpointUri = "https://outlook.office365.com/api/v1.0";
            serviceId = "O365_EXCHANGE";
            serviceName = "Office 365 Exchange";
            serviceResourceId = "https://outlook.office365.com/";
        },
                {
            "@odata.editLink" = "services('Mail@O365_EXCHANGE')";
            "@odata.id" = "https://api.office.com/discovery/v1.0/me/services('Mail@O365_EXCHANGE')";
            "@odata.type" = "#Microsoft.DiscoveryServices.ServiceInfo";
            capability = Mail;
            entityKey = "Mail@O365_EXCHANGE";
            providerId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
            providerName = Microsoft;
            serviceAccountType = 2;
            serviceApiVersion = "v1.0";
            serviceEndpointUri = "https://outlook.office365.com/api/v1.0";
            serviceId = "O365_EXCHANGE";
            serviceName = "Office 365 Exchange";
            serviceResourceId = "https://outlook.office365.com/";
        },
                {
            "@odata.editLink" = "services('Calendar@O365_EXCHANGE')";
            "@odata.id" = "https://api.office.com/discovery/v1.0/me/services('Calendar@O365_EXCHANGE')";
            "@odata.type" = "#Microsoft.DiscoveryServices.ServiceInfo";
            capability = Calendar;
            entityKey = "Calendar@O365_EXCHANGE";
            providerId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
            providerName = Microsoft;
            serviceAccountType = 2;
            serviceApiVersion = "v1.0";
            serviceEndpointUri = "https://outlook.office365.com/api/v1.0";
            serviceId = "O365_EXCHANGE";
            serviceName = "Office 365 Exchange";
            serviceResourceId = "https://outlook.office365.com/";
        }
    );
}

As mentioned before, we have to declare what permissions are needed by our application. But what happens exactly for the user if he’s not agree with these permissions ?

When ADAL tries to acquire an access token and if the user account is not stored in the same directory as your tenant, he must consent to authorize the application for the permissions you are requested.

ADAL - User ConsentIf the user doesn’t consent to authorize your application to use the required permissions, ADAL returns an AD_FAILED status when you call acquireTokenWithResource. The consent is required only once. For subsequent requests the same consent is used by ADAL.

Now you are theorically able to use ADAL and Office 365 Discovery REST API to authenticate and retrieve information about available services for the current user  😉

Opening a document with Office Mobile from your own application

You want to write your own mobile application and need to open Office documents from it ?

Since Microsoft has released Office Mobile on iOS and Android platforms, you probably asked how to open Office documents with these applications from your own application.

After few hours of searches on Google, I didn’t find any information on this subject. So I decided to investigate by myself and I found the solution by using Office Web Apps and by analysing how it works when you click on the option “Edit in Word“.

Office Mobile applications expose some special schemes (like http or https) which allow you to open documents and passing information to the application.

Here is the list of schemes I found through my analysis of Office Web Apps :

  • ms-word
  • ms-excel
  • ms-powerpoint
  • onenote

My need is to open in Office Mobile, some documents which are stored in Office 365, but are presented through a dedicated mobile interface (running on iOS). Opening these files with Office Mobile allow users to read and edit them, and save modifications directly to Office 365.

By using previous schemes, it’s easy to build an URL and to open it using the following code :

NSURL *documentURL = [NSURL URLWithString:@"ms-word:https://tenant.sharepoint.com/Shared%20Documents/Document.docx"];
if ([[UIApplication sharedApplication] canOpenURL:documentURL]) {
    [[UIApplication sharedApplication] openURL:documentURL];
}

As you can see, you just have to concatenate the desired scheme (ms-word: in my case) with the URL of the document.

To ensure that Office Mobile is installed on the device, you can check if the application is able to open it with the call to canOpenURL.

When your application execute the openURL method, the right application is executed and the document is opened. If you’re not authenticated inside the application, you’re invited to do it.

Office Mobile - View Mode

When the document was opened, you probably noticed that it was opened in read-only mode.

It’s the default mode, when you just concatenate an URL to the scheme. But what to do if you want to open the document in edit mode ?

I also found the solution by analysing Office Web Apps  😉

You can specify some additional information between the scheme and the URL of the document.

NSURL *documentURL = [NSURL URLWithString:@"ms-word:ofe%7Cu%7Chttps://tenant.sharepoint.com/Shared%20Documents/Document.docx"]

As you can see above, “ofe%7Cu%7C” was inserted to tell Office Mobile that you want to edit the file (I guess “ofe” means “open for edit”). It’s also possible to use “ofv” (I guess it means “open for view”) in the same manner.

When you run the same code as previously, but with the new URL, the yellow bar below the ribbon has disappeared and you can edit the document without any further action.

Office Mobile - Edit mode

Note that after some tests, this also works with Office for Mac (and probably with Office for Windows).

Enjoy  😉