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
https://developer.apple.com/documentation/foundation/nsitemprovider/1403900-loaditemcompletionHandler
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 toNSString
orNSAttributedString
. An item provider can perform simple type conversions of the data to the class you specify, such as fromNSURL
toNSData
orFileWrapper
, or fromNSData
toUIImage
(in iOS) orNSImage
(in macOS). If the data could not be retrieved or coerced to the specified class, an error is passed to the completion block’s.
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() }
}
}
}
Voilà on s’arrête ici, j’espère que cet article vous a aidé. Dans tout les cas vos commentaires sont les bienvenus.