Skip to content

Latest commit

 

History

History
488 lines (397 loc) · 17.8 KB

2014-04-21-uiactivityviewcontroller.md

File metadata and controls

488 lines (397 loc) · 17.8 KB
title author category excerpt
UIActivityViewController
Mattt Thompson
Cocoa
The relationship between code and data has long been a curious one.

The relationship between code and data has long been a curious one.

Certain programming languages, such as Lisp, Io, and Mathematica are homoiconic, meaning that their code is represented as a data primitive, which itself can be manipulated in code. Most other languages, including Objective-C, however, create a strict boundary between the two, shying away from eval() and other potentially dangerous methods of dynamic instructing loading.

This tension between code and data is brought to a whole new level when the data in question is too large or unwieldy to represent as anything but a byte stream. The question of how to encode, decode, and interpret the binary representation of images, documents, and media has been ongoing since the very first operating systems.

The Core Services framework on OS X and Mobile Core Services framework on iOS provide functions that identify and categorize data types by file extension and MIME type, according to Universal Type Identifiers. UTIs provide an extensible, hierarchical categorization system, which affords the developer great flexibility in handling even the most exotic file types. For example, a Ruby source file (.rb) is categorized as Ruby Source Code > Source Code > Text > Content > Data; a QuickTime Movie file (.mov) is categorized as Video > Movie > Audiovisual Content > Content > Data.

UTIs have worked reasonably well within the filesystem abstraction of the desktop. However, in a mobile paradigm, where files and directories are hidden from the user, this breaks down quickly. And, what's more, the rise of cloud services and social media has placed greater importance on remote entities over local files. Thus, a tension between UTIs and URLs.

It's clear that we need something else. Could UIActivityViewController be the solution we so desperately seek?


UIActivityViewController, introduced in iOS 6, provides a unified services interface for sharing and performing actions on data within an application.

Given a collection of actionable data, a UIActivityViewController instance is created as follows:

NSString *string = ...;
NSURL *URL = ...;

UIActivityViewController *activityViewController =
  [[UIActivityViewController alloc] initWithActivityItems:@[string, URL]
                                    applicationActivities:nil];
[navigationController presentViewController:activityViewController
                                      animated:YES
                                    completion:^{
  // ...
}];

This would present the following at the bottom of the screen:

UIActivityViewController

By default, UIActivityViewController will show all available services supporting the provided items, but certain activity types can be excluded:

activityViewController.excludedActivityTypes = @[UIActivityTypePostToFacebook];

Activity types are divided up into "action" and "share" types:

UIActivityCategoryAction

  • UIActivityTypePrint
  • UIActivityTypeCopyToPasteboard
  • UIActivityTypeAssignToContact
  • UIActivityTypeSaveToCameraRoll
  • UIActivityTypeAddToReadingList
  • UIActivityTypeAirDrop

UIActivityCategoryShare

  • UIActivityTypeMessage
  • UIActivityTypeMail
  • UIActivityTypePostToFacebook
  • UIActivityTypePostToTwitter
  • UIActivityTypePostToFlickr
  • UIActivityTypePostToVimeo
  • UIActivityTypePostToTencentWeibo
  • UIActivityTypePostToWeibo

Each activity type supports a number of different data types. For example, a Tweet might be composed of an NSString, along with an attached image and/or URL.

Supported Data Types by Activity Type

Activity Type String Attributed String URL Data Image Asset Other
Post To Facebook
Post To Twitter
Post To Weibo
Message ✓* ✓* ✓* sms:// NSURL
Mail ✓+ ✓+ ✓+
Print ✓+ ✓+ UIPrintPageRenderer, UIPrintFormatter, & UIPrintInfo
Copy To Pasteboard UIColor, NSDictionary
Assign To Contact
Save To Camera Roll
Add To Reading List
Post To Flickr
Post To Vimeo
Post To Tencent Weibo
AirDrop

<UIActivityItemSource> & UIActivityItemProvider

Similar to how a pasteboard item can be used to provide data only when necessary, in order to avoid excessive memory allocation or processing time, activity items can be of a custom type.

Any object conforming to <UIActivityItemSource>, including the built-in UIActivityItemProvider class, can be used to dynamically provide different kinds of data depending on the activity type.

<UIActivityItemSource>

Getting the Data Items

  • activityViewControllerPlaceholderItem:
  • activityViewController:itemForActivityType:

Providing Information About the Data Items

  • activityViewController:subjectForActivityType:
  • activityViewController:dataTypeIdentifierForActivityType:
  • activityViewController:thumbnailImageForActivityType:suggestedSize:

One example of how this could be used is to customize a message, depending on whether it's to be shared on Facebook or Twitter.

- (id)activityViewController:(UIActivityViewController *)activityViewController
         itemForActivityType:(NSString *)activityType
{
    if ([activityType isEqualToString:UIActivityTypePostToFacebook]) {
        return NSLocalizedString(@"Like this!");
    } else if ([activityType isEqualToString:UIActivityTypePostToTwitter]) {
        return NSLocalizedString(@"Retweet this!");
    } else {
        return nil;
    }
}

Creating a Custom UIActivity

In addition to the aforementioned system-provided activities, its possible to create your own activity.

As an example, let's create a custom activity type that takes an image URL and applies a mustache to it using mustache.me.

Jony Ive Before Jony Ive After
Before After

First, we define a reverse-DNS identifier for the activity type, specify the category as UIActivityCategoryAction, and provide a localized title & iOS version appropriate image:

static NSString * const HIPMustachifyActivityType = @"com.nshipster.activity.Mustachify";
#pragma mark - UIActivity

+ (UIActivityCategory)activityCategory {
    return UIActivityCategoryAction;
}

- (NSString *)activityType {
    return HIPMustachifyActivityType;
}

- (NSString *)activityTitle {
    return NSLocalizedString(@"Mustachify", nil);
}

- (UIImage *)activityImage {
    if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
        return [UIImage imageNamed:@"MustachifyUIActivity7"];
    } else {
        return [UIImage imageNamed:@"MustachifyUIActivity"];
    }
}

Next, we create a helper function, HIPMatchingURLsInActivityItems, which returns an array of any image URLs of the supported type.

static NSArray * HIPMatchingURLsInActivityItems(NSArray *activityItems) {
    return [activityItems filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:
    ^BOOL(id item, __unused NSDictionary *bindings) {
        if ([item isKindOfClass:[NSURL class]] &&
            ![(NSURL *)item isFileURL]) {
            return [[(NSURL *)item pathExtension] caseInsensitiveCompare:@"jpg"] == NSOrderedSame ||
            [[(NSURL *)item pathExtension] caseInsensitiveCompare:@"png"] == NSOrderedSame;
        }

        return NO;
    }]];
}

This function is then used in -canPerformWithActivityItems: and prepareWithActivityItems: to get the mustachio'd image URL of the first PNG or JPEG, if any.

- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
    return [HIPMatchingURLsInActivityItems(activityItems) count] > 0;
}

- (void)prepareWithActivityItems:(NSArray *)activityItems {
    static NSString * const HIPMustachifyMeURLFormatString = @"http://mustachify.me/%d?src=%@";

    self.imageURL = [NSURL URLWithString:[NSString stringWithFormat:HIPMustachifyMeURLFormatString, self.mustacheType, [HIPMatchingURLsInActivityItems(activityItems) firstObject]]];
}

Our webservice provides a variety of mustache options, which are defined in an NS_ENUM:

typedef NS_ENUM(NSInteger, HIPMustacheType) {
    HIPMustacheTypeEnglish,
    HIPMustacheTypeHorseshoe,
    HIPMustacheTypeImperial,
    HIPMustacheTypeChevron,
    HIPMustacheTypeNatural,
    HIPMustacheTypeHandlebar,
};

Finally, we provide a UIViewController to display the image. For this example, a simple UIWebView controller suffices.

@interface HIPMustachifyWebViewController : UIViewController <UIWebViewDelegate>
@property (readonly, nonatomic, strong) UIWebView *webView;
@end
- (UIViewController *)activityViewController {
    HIPMustachifyWebViewController *webViewController = [[HIPMustachifyWebViewController alloc] init];

    NSURLRequest *request = [NSURLRequest requestWithURL:self.imageURL];
    [webViewController.webView loadRequest:request];

    return webViewController;
}

To use our brand new mustache activity, we simply pass it in the UIActivityViewController initializer:

HIPMustachifyActivity *mustacheActivity = [[HIPMustachifyActivity alloc] init];
UIActivityViewController *activityViewController =
  [[UIActivityViewController alloc] initWithActivityItems:@[imageURL]
                                    applicationActivities:@[mustacheActivity]];

Invoking Actions Manually

Now is a good time to be reminded that while UIActivityViewController allows users to perform actions of their choosing, sharing can still be invoked manually, when the occasion arises.

So for completeness, here's how one might go about performing some of these actions manually:

Open URL

NSURL *URL = [NSURL URLWithString:@"http://nshipster.com"];
[[UIApplication sharedApplication] openURL:URL];

System-supported URL schemes include: mailto:, tel:, sms:, and maps:.

Add to Safari Reading List

@import SafariServices;

NSURL *URL = [NSURL URLWithString:@"http://nshipster.com/uiactivityviewcontroller"];
[[SSReadingList defaultReadingList] addReadingListItemWithURL:URL
                                                        title:@"NSHipster"
                                                  previewText:@"..."
                                                        error:nil];

Add to Saved Photos

UIImage *image = ...;
id completionTarget = self;
SEL completionSelector = @selector(didWriteToSavedPhotosAlbum);
void *contextInfo = NULL;
UIImageWriteToSavedPhotosAlbum(image, completionTarget, completionSelector, contextInfo);

Send SMS

@import MessageUI;

MFMessageComposeViewController *messageComposeViewController = [[MFMessageComposeViewController alloc] init];
messageComposeViewController.mailComposeDelegate = self;
messageComposeViewController.recipients = @[@"mattt@nshipster•com"];
messageComposeViewController.body = @"Lorem ipsum dolor sit amet";
[navigationController presentViewController:messageComposeViewController animated:YES completion:^{
    // ...
}];

Send Email

@import MessageUI;

MFMailComposeViewController *mailComposeViewController = [[MFMailComposeViewController alloc] init];
[mailComposeViewController setToRecipients:@[@"mattt@nshipster•com"]];
[mailComposeViewController setSubject:@"Hello"];
[mailComposeViewController setMessageBody:@"Lorem ipsum dolor sit amet"
                                   isHTML:NO];
[navigationController presentViewController:mailComposeViewController animated:YES completion:^{
    // ...
}];

Post Tweet

@import Twitter;

TWTweetComposeViewController *tweetComposeViewController =
    [[TWTweetComposeViewController alloc] init];
[tweetComposeViewController setInitialText:@"Lorem ipsum dolor sit amet."];
[self.navigationController presentViewController:tweetComposeViewController
                                        animated:YES
                                      completion:^{
    //...
}];

IntentKit

While all of this is impressive and useful, there is a particular lacking in the activities paradigm in iOS, when compared to the rich Intents Model found on Android.

On Android, apps can register for different intents, to indicate that they can be used for Maps, or as a Browser, and be selected as the default app for related activities, like getting directions, or bookmarking a URL.

While iOS lacks the extensible infrastructure to support this, a 3rd-party library called IntentKit, by @lazerwalker (of f*ingblocksyntax.com fame), is an interesting example of how we might narrow the gap ourselves.

IntentKit

Normally, a developer would have to do a lot of work to first, determine whether a particular app is installed, and how to construct a URL to support a particular activity.

IntentKit consolidates the logic of connecting to the most popular Web, Maps, Mail, Twitter, Facebook, and Google+ clients, in a UI similar to UIActivityViewController.

Anyone looking to take their sharing experience to the next level should definitely give this a look.


There is a strong argument to be made that the longterm viability of iOS as a platform depends on sharing mechanisms like UIActivityViewController. As the saying goes, "Information wants to be free". And anything that stands in the way of federation will ultimately lose to something that does not.

The future prospects of public remote view controller APIs gives me hope for the future of sharing on iOS. For now, though, we could certainly do much worse than UIActivityViewController.