Résoudre l’erreur _EXSinkLoadOperator de NSItemProvider

Contexte

Si vous arrivez sur cet article c’est probablement que la console d’Xcode vous affiche l’erreur que vous voyez sur l’image ci-dessus. Une erreur semblant provenir de la classe _EXSinkLoadOperator. Cette classe étant totalement inconnue et sortant surement des arcanes les plus profondes d’iOS vous avez alors fait une recherche Google et, on ne vas pas se mentir, si je déterre mon blog après presque 9 ans d’absence c’est que cette recherche Google ne donne pas grand chose. Enfin jusqu’à maintenant et l’article que vous lisez actuellement car je vous l’assure il contient une (la ?) solution !

Si vous avez cette erreur c’est que vous avez une AppExtension dans votre projet et un UIViewController en Swift (en Objective-C vous n’aurez normalement pas le problème) qui tente de récupérer le contenu partagé via la propriété extensionContext de type NSExtensionContext. Le problème est alors très proche car il se situe plus ou moins dans l’appel à la fonction loadItem(forTypeIdentifier:options:completionHandler:). À ce moment là, lorsque vous exécutez votre extension (afin d’avoir les logs la concernant dans la console d’Xcode) vous voyez apparaitre l’erreur de l’image ci-dessus.

Problème

Vous le comprenez rapidement, la propriété expectedValueClass donnée en paramètre à la fonction est nil, or celle-ci attend un type de classe pour faire la conversion de ce que l’on veut récupérer. Comme vous n’appelez pas directement cette fonction mais qu’iOS le fait au travers de la fonction loadItem(forTypeIdentifier:options:completionHandler:) ci-dessus et que celle-ci ne permet pas de forcer le type ça semble compliqué de résoudre l’erreur. Même si oui ce n’est qu’une erreur dans les logs et que tout fonctionne quand même. Mais quand on aime faire les choses proprement … 🤷🏻‍♂️

Recherche

Du coup vous regardez la documentation d’Apple pour voir si vous avez raté quelque chose. Vous lisez d’ailleurs sur la documentation de la fonction ce qui suit :

The type information for the first parameter of your completionHandler block should be set to the class of the expected type. For example, when requesting text data, you might set the type of the first parameter to NSString or NSAttributedString. An item provider can perform simple type conversions of the data to the class you specify, such as from NSURL to NSData or FileWrapper, or from NSData to UIImage (in iOS) or NSImage (in macOS). If the data could not be retrieved or coerced to the specified class, an error is passed to the completion block’s.

https://developer.apple.com/documentation/foundation/nsitemprovider/1403900-loaditem

C’est bien beau mais tenter de forcer le type du paramètre que l’on s’attend a recevoir en entrée du bloc de completion ne résout rien quand on est en Swift, au contraire vous aurez surement une erreur « Type of expression is ambiguous without a type annotation« .

Vous jetez un nouveau coup d’oeil à la documentation, cette fois-ci celle de NSItemProvider et vous voyez des fonctions nommées canLoadObject(ofClass:) et loadObject(ofClass:completionHandler:) alors là cette fois c’est bon vous vous dites voilà les fonctions à utiliser quand on est en Swift car elle permettent de spécifier très clairement le type d’instance que l’on s’attends à récupérer ! Non pas du tout ! Après avoir testé vous avez toujours le même comportement ! Dingue ! Allez-vous devoir utiliser les méthodes Objective-C pour enfin éviter cette fichue erreur dans vos logs ?! …. Eh bien … oui ! 🙃

Solution

Ca y est vous avez accepté l’idée de mettre de l’Objective-C dans votre beau projet en Swift ? Alors je vous montre ce que vous devez ajouter.

Il vous faut ajouter une extension de NSItemProvider (appelée une catégorie en Objective-C) et vous adapterez le code en fonction de votre besoin. Si vous devez recevoir une image le type sera UIImage par exemple, mais si comme moi vous devez récupérer une URL (qui peut être sous forme de String) alors vous pouvez copier-coller sans modification.

Dans le header (NSItemProvider+Utils.h) vous allez ajouter ceci :

@import UIKit;

@interface NSItemProvider (Utils)

typedef void (^NSItemProviderCompletionHandlerForURL)(NSURL *url, NSError *error);
typedef void (^NSItemProviderCompletionHandlerForURLString)(NSString *urlString, NSError *error);

- (void)loadURLForTypeIdentifier:(NSString *)typeIdentifier
                         options:(NSDictionary *)options
               completionHandler:(NSItemProviderCompletionHandlerForURL)completionHandler;

- (void)loadURLStringForTypeIdentifier:(NSString *)typeIdentifier
                               options:(NSDictionary *)options
                     completionHandler:(NSItemProviderCompletionHandlerForURLString)completionHandler;

@end

Puis dans l’implémentation (NSItemProvider+Utils.m) ceci:

#import "NSItemProvider+Utils.h"

@implementation NSItemProvider (Utils)

- (void)loadURLForTypeIdentifier:(NSString *)typeIdentifier options:(NSDictionary *)options completionHandler:(NSItemProviderCompletionHandlerForURL)completionHandler {
    [self loadItemForTypeIdentifier:typeIdentifier
                            options:options
                  completionHandler:completionHandler];
}

- (void)loadURLStringForTypeIdentifier:(NSString *)typeIdentifier options:(NSDictionary *)options completionHandler:(NSItemProviderCompletionHandlerForURLString)completionHandler {
    [self loadItemForTypeIdentifier:typeIdentifier
                            options:options
                  completionHandler:completionHandler];
}

@end

Maintenant il vas y avoir une distinction selon si vous devez appeler les fonctions de l’extension depuis du code dans un xcodeproj ou depuis du code dans un SPM.

Vous êtes dans un xcodeproj

Dans ce cas c’est le plus simple et rapide, il vous suffit de créer un bridging header ({YourProject}-Bridging-Header.h) (si vous n’en avez pas déjà un) et d’y ajoutez :

#import "NSItemProvider+Swift.h"

Vous êtes dans un SPM

Dans ce cas là c’est un peu plus complexe mais voici la marche à suivre, suivez le guide !

Commencez par ajouter un dossier « NSItemProviderUtils » dans le dossier « Sources » de votre SPM. Puis ajoutez un nouveau dossier « include » dans celui-ci. Placez-ensuite le fichier .h dans le dossier « include » et le fichier .m dans le dossier « NSItemProviderUtils ».. Vous devriez alors avoir une structure comme celle-ci:

Ensuite ouvrez le fichier « Package » de votre SPM afin d’y ajouter une nouvelle target (.target(name:)) et une dépendance sur cette nouvelle target dans celle existante (.targetItem(name:condition:). Donnez lui un nom comme « NSItemProviderUtils » et le même aux deux endroits. Ce qui devrez vous donner quelque chose de similaire à ça :

Enfin il ne vous reste plus qu’à ajouter un import NSItemProvideUtils dans le fichier Swift où vous souhaitez utiliser cette extension !

Voici le code que j’avais au départ et qui générait l’erreur puis le code que j’ai maintenant (utilisant l’extension, ainsi que les UTType pour les identifiant à la place de String en dure.

Ancien code qui génère l’erreur :

public override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    /* Not nil only when presented by the app extension */
    guard let extensionContext = extensionContext else { return }

    /* Retrieving of the URL from parameters given by the app-extension to prefill the URL textfield and launch the Youtube API request. */
    guard
        let contextItem = extensionContext.inputItems.first as? NSExtensionItem,
        let itemProvider = contextItem.attachments?.first as? NSItemProvider,
        itemProvider.hasItemConformingToTypeIdentifier("public.url") || itemProvider.hasItemConformingToTypeIdentifier("public.plain-text")
    else {
        let error = NSError(domain: "app.veedz", code: 0, userInfo: [NSLocalizedDescriptionKey: "No shared information found."])
        extensionContext.cancelRequest(withError: error)
        return
    }

    if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
        itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { url, error in
            if let prefilledURL = url as? URL {
                self.prefilledURL = prefilledURL
                DispatchQueue.main.async { self.tableView.reloadData() }
            } else {
                self.prefilledURL = nil
            }
        }
    } else if itemProvider.hasItemConformingToTypeIdentifier("public.plain-text") {
        itemProvider.loadItem(forTypeIdentifier: "public.plain-text", options: nil) { urlString, error in
            if let prefilledURLString = urlString as? String, let prefilledURL = URL(string: prefilledURLString) {
                self.prefilledURL = prefilledURL
                DispatchQueue.main.async { self.tableView.reloadData() }
            } else {
                self.prefilledURL = nil
            }
        }
    }
}

Nouveau code sans erreur :

public override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    /* Not nil only when presented by the app extension */
    guard let extensionContext = extensionContext else { return }

    /* Retrieving of the URL from parameters given by the app-extension to prefill the URL textfield and launch the Youtube API request. */
    guard
        let contextItem = extensionContext.inputItems.first as? NSExtensionItem,
        let itemProvider = contextItem.attachments?.first as? NSItemProvider,
        itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) || itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier)
    else {
        let error = NSError(domain: "app.veedz", code: 0, userInfo: [NSLocalizedDescriptionKey: "No shared information found."])
        extensionContext.cancelRequest(withError: error)
        return
    }

    if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
        itemProvider.loadURL(forTypeIdentifier: UTType.url.identifier) { [weak self] url, error in
            guard let self else { return }
            self.prefilledURL = url
            DispatchQueue.main.async { self.tableView.reloadData() }
        }
    } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
        itemProvider.loadURLString(forTypeIdentifier: UTType.plainText.identifier) { [weak self] urlString, error in
            guard
                let self,
                let urlString,
                let prefilledURL = URL(string: urlString)
            else {
                self?.prefilledURL = nil
                return
            }
            self.prefilledURL = prefilledURL
            DispatchQueue.main.async { self.tableView.reloadData() }
        }
    }
}
Code avant et après

Voilà on s’arrête ici, j’espère que cet article vous a aidé. Dans tout les cas vos commentaires sont les bienvenus.

En savoir plus sur Thibault Le Cornec

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Continue reading