Slide 1

Slide 1 text

The unofficial guide to building Action Extensions

Slide 2

Slide 2 text

Extensions • Set up as a separate target • Out of process of the host app or the containing app • Has its own sandbox • Can share data with the containing app through App Groups • Can share data with the host app only through XPC

Slide 3

Slide 3 text

Action Extensions • Visible in the share sheet in the second row • “An Action extension helps users view or transform content originating in a host app.” [1] • On launch the extension gets the data to be shared from the host app through a NSExtensionContext [1]: https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/Action.html

Slide 4

Slide 4 text

Today’s Talk • How to implement an action extension in theory • How to make that implementation work in practice • I will not cover the host app’s integration

Slide 5

Slide 5 text

The Theory

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

The Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(UIImage *image, NSError *error) {
 if(image) {
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 [imageView setImage:image];
 }];
 }
 }];
 }
 }
 }

Slide 8

Slide 8 text

The Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(UIImage *image, NSError *error) {
 if(image) {
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 [imageView setImage:image];
 }];
 }
 }];
 }
 }
 }

Slide 9

Slide 9 text

The Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(UIImage *image, NSError *error) {
 if(image) {
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 [imageView setImage:image];
 }];
 }
 }];
 }
 }
 }

Slide 10

Slide 10 text

The Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(UIImage *image, NSError *error) {
 if(image) {
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 [imageView setImage:image];
 }];
 }
 }];
 }
 }
 }

Slide 11

Slide 11 text

The Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(UIImage *image, NSError *error) {
 if(image) {
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 [imageView setImage:image];
 }];
 }
 }];
 }
 }
 }

Slide 12

Slide 12 text

The Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(UIImage *image, NSError *error) {
 if(image) {
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 [imageView setImage:image];
 }];
 }
 }];
 }
 }
 }

Slide 13

Slide 13 text

The Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(UIImage *image, NSError *error) {
 if(image) {
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 [imageView setImage:image];
 }];
 }
 }];
 }
 }
 }

Slide 14

Slide 14 text

The Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(UIImage *image, NSError *error) {
 if(image) {
 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
 [imageView setImage:image];
 }];
 }
 }];
 }
 }
 }

Slide 15

Slide 15 text

Modifying the Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(NSData *data, NSError *error) {
 NSURL* targetURL = …;
 [data writeToURL:targetURL atomically:YES];
 }];
 }
 }
 }

Slide 16

Slide 16 text

Modifying the Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(NSData *data, NSError *error) {
 NSURL* targetURL = …;
 [data writeToURL:targetURL atomically:YES];
 }];
 }
 }
 }

Slide 17

Slide 17 text

Modifying the Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(NSData *data, NSError *error) {
 NSURL* targetURL = …;
 [data writeToURL:targetURL atomically:YES];
 }];
 }
 }
 }

Slide 18

Slide 18 text

Modifying the Loop for (NSExtensionItem *item in self.extensionContext.inputItems) {
 for (NSItemProvider *prov in item.attachments) {
 if ([prov hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
 __weak UIImageView *imageView = self.imageView;
 [prov loadItemForTypeIdentifier:(NSString *)kUTTypeImage
 options:nil
 completionHandler:^(NSData *data, NSError *error) {
 NSURL* targetURL = …;
 [data writeToURL:targetURL atomically:YES];
 }];
 }
 }
 }

Slide 19

Slide 19 text

Demo

Slide 20

Slide 20 text

Pitfalls • Data might come in various different formats even for the same type identifier • Some fileURLs seem to not be accessible from the sandbox • Specify the loaded item as id and look at the data you get

Slide 21

Slide 21 text

Error Handling • What can go wrong, will go wrong • If you use Swift: Don’t do any force unwrapping or fatalError() calls • If you use Objective-C: Be super careful with everything that is declared nullable

Slide 22

Slide 22 text

Error Handling • Sometimes your extension might even get launched but does not receive any items at all • Handle absolutely everything that could happen, from an API point of view, not from a documentation point of view • You are dealing with random apps that could do random things

Slide 23

Slide 23 text

Radar Time • Safari shares images as Data, other apps as URL (rdar://29924023) • Message.app shares image file url not accessible from inside an action extension (rdar:// 29918507) • Photos.app advertises live photo to action extensions which then can't be accessed (rdar:// 29924331) • Messages.app advertises live photos to action extensions as public.image and is sharing a jpeg (rdar://29924679) • Host apps interfere with the tint color of extensions (rdar://30141950) • There needs to be a document describing the differences of NSExtensionItem, NSItemProvider and the items from an NSItemProvider (rdar://30184485)

Slide 24

Slide 24 text

Radar Time • P.s.: Please file radars or dupe the ones you want to get fixed!

Slide 25

Slide 25 text

Radar Time • P.s.: Please file radars or dupe the ones you want to get fixed!

Slide 26

Slide 26 text

Radar Time • P.s.: Please file radars or dupe the ones you want to get fixed!

Slide 27

Slide 27 text

–Tanya Gupta, Engineering Manager at Apple “Sometimes they say, well it’s a really obvious problem! I’m sure you have 12 copies of the bug […] should I still file a bug report? Yes, you should still file a bug report. Better have 5 copies of a bug than none at all. At Apple, if an issue is not tracked using a bug report, it essentially does not exist for us.”

Slide 28

Slide 28 text

Radar Time • Sample projects help you to identify the problem • Use OpenRadar if you can

Slide 29

Slide 29 text

Radar Time • Safari shares images as Data, other apps as URL (rdar://29924023) • Message.app shares image file url not accessible from inside an action extension (rdar:// 29918507) • Photos.app advertises live photo to action extensions which then can't be accessed (rdar:// 29924331) • Messages.app advertises live photos to action extensions as public.image and is sharing a jpeg (rdar://29924679) • Host apps interfere with the tint color of extensions (rdar://30141950) • There needs to be a document describing the differences of NSExtensionItem, NSItemProvider and the items from an NSItemProvider (rdar://30184485)

Slide 30

Slide 30 text

Radar Time • Safari shares images as Data, other apps as URL (rdar://29924023) • Message.app shares image file url not accessible from inside an action extension (rdar:// 29918507) • Photos.app advertises live photo to action extensions which then can't be accessed (rdar:// 29924331) • Messages.app advertises live photos to action extensions as public.image and is sharing a jpeg (rdar://29924679) • Host apps interfere with the tint color of extensions (rdar://30141950) • There needs to be a document describing the differences of NSExtensionItem, NSItemProvider and the items from an NSItemProvider (rdar://30184485)

Slide 31

Slide 31 text

Demo

Slide 32

Slide 32 text

UIAppearance • Either ensure your extension can deal with every possible appearance • Or set NSExtensionOverridesHostUIAppearance to YES in the NSExtension dictionary in the extension’s Info.plist

Slide 33

Slide 33 text

UIAppearance • Either ensure your extension can deal with every possible appearance • Or set NSExtensionOverridesHostUIAppearance to YES in the NSExtension dictionary in the extension’s Info.plist

Slide 34

Slide 34 text

Getting ready to ship • Set the activation rule in you extension’s Info.plist • TRUEPREDICATE is great for debugging

Slide 35

Slide 35 text

Getting ready to ship • Set the activation rule in you extension’s Info.plist • TRUEPREDICATE is great for debugging

Slide 36

Slide 36 text

Getting ready to ship SUBQUERY (
 extensionItems,
 $extensionItem,
 SUBQUERY (
 $extensionItem.attachments,
 $attachment,
 (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image") AND
 NOT (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO
 "com.pspdfkit.viewer.blocker")
 ).@count == $extensionItem.attachments.@count
 ).@count >= 1

Slide 37

Slide 37 text

Getting ready to ship SUBQUERY (
 extensionItems,
 $extensionItem,
 SUBQUERY (
 $extensionItem.attachments,
 $attachment,
 (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image") AND
 NOT (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO
 "com.pspdfkit.viewer.blocker")
 ).@count == $extensionItem.attachments.@count
 ).@count >= 1

Slide 38

Slide 38 text

Getting ready to ship SUBQUERY (
 extensionItems,
 $extensionItem,
 SUBQUERY (
 $extensionItem.attachments,
 $attachment,
 (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image") AND
 NOT (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO
 "com.pspdfkit.viewer.blocker")
 ).@count == $extensionItem.attachments.@count
 ).@count >= 1

Slide 39

Slide 39 text

Getting ready to ship SUBQUERY (
 extensionItems,
 $extensionItem,
 SUBQUERY (
 $extensionItem.attachments,
 $attachment,
 (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image") AND
 NOT (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO
 "com.pspdfkit.viewer.blocker")
 ).@count == $extensionItem.attachments.@count
 ).@count >= 1

Slide 40

Slide 40 text

Getting ready to ship SUBQUERY (
 extensionItems,
 $extensionItem,
 SUBQUERY (
 $extensionItem.attachments,
 $attachment,
 (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image") AND
 NOT (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO
 "com.pspdfkit.viewer.blocker")
 ).@count == $extensionItem.attachments.@count
 ).@count >= 1

Slide 41

Slide 41 text

Getting ready to ship SUBQUERY (
 extensionItems,
 $extensionItem,
 SUBQUERY (
 $extensionItem.attachments,
 $attachment,
 (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image") AND
 NOT (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO
 "com.pspdfkit.viewer.blocker")
 ).@count == $extensionItem.attachments.@count
 ).@count >= 1

Slide 42

Slide 42 text

Getting ready to ship SUBQUERY (
 extensionItems,
 $extensionItem,
 SUBQUERY (
 $extensionItem.attachments,
 $attachment,
 (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" OR
 ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image") AND
 NOT (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO
 "com.pspdfkit.viewer.blocker")
 ).@count == $extensionItem.attachments.@count
 ).@count >= 1

Slide 43

Slide 43 text

Reading Material • The Struggle with Action Extensions
 https://pspdfkit.com/blog/2017/action-extension/ • Hiding Your Action and Share Extensions In Your Own Apps
 https://pspdfkit.com/blog/2016/hiding-action-share-extensions-in-your- own-apps/ • Writing good bug reports
 https://pspdfkit.com/blog/2016/writing-good-bug-reports/

Slide 44

Slide 44 text

Thank you Michael Ochs @_mochs https://pspdfkit.com/blog/