title | author | category | excerpt | status | ||||
---|---|---|---|---|---|---|---|---|
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:
let string: String = ...
let URL: NSURL = ...
let activityViewController = UIActivityViewController(activityItems: [string, URL], applicationActivities: nil)
navigationController?.presentViewController(activityViewController, animated: true) {
// ...
}
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]({{ site.asseturl }}/uiactivityviewcontroller.png)
By default, UIActivityViewController
will show all available services supporting the provided items, but certain activity types can be excluded:
activityViewController.excludedActivityTypes = [UIActivityTypePostToFacebook]
activityViewController.excludedActivityTypes = @[UIActivityTypePostToFacebook];
Activity types are divided up into "action" and "share" types:
UIActivityTypePrint
UIActivityTypeCopyToPasteboard
UIActivityTypeAssignToContact
UIActivityTypeSaveToCameraRoll
UIActivityTypeAddToReadingList
UIActivityTypeAirDrop
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.
Activity Type | String | Attributed String | URL | Data | Image | Asset | Other |
---|---|---|---|---|---|---|---|
Post To Facebook | ✓ | ✓ | ✓ | ✓ | |||
Post To Twitter | ✓ | ✓ | ✓ | ✓ | |||
Post To Weibo | ✓ | ✓ | ✓ | ✓ | ✓ | ||
Message | ✓ | ✓ | ✓* | ✓* | ✓* | sms:// NSURL | |
✓+ | ✓+ | ✓+ | |||||
✓+ | ✓+ | 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 | ✓ | ✓ | ✓ | ✓ | ✓ |
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.
activityViewControllerPlaceholderItem:
activityViewController:itemForActivityType:
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.
func activityViewController(activityViewController: UIActivityViewController, itemForActivityType activityType: String) -> AnyObject? {
if activityType == UIActivityTypePostToFacebook {
return NSLocalizedString("Like this!", comment: "comment")
} else if activityType == UIActivityTypePostToTwitter {
return NSLocalizedString("Retweet this!", comment: "comment")
} else {
return nil
}
}
- (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;
}
}
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.
Before | After |
First, we define a reverse-DNS identifier for the activity type:
let HIPMustachifyActivityType = "com.nshipster.activity.Mustachify"
static NSString * const HIPMustachifyActivityType = @"com.nshipster.activity.Mustachify";
Then specify the category as UIActivityCategoryAction
and provide a localized title & iOS version appropriate image:
// MARK: - UIActivity
override class func activityCategory() -> UIActivityCategory {
return .Action
}
override func activityType() -> String? {
return HIPMustachifyActivityType
}
override func activityTitle() -> String? {
return NSLocalizedString("Mustachify", comment: "comment")
}
override func activityImage() -> UIImage? {
if #available(iOS 7.0, *) {
return UIImage(named: "MustachifyUIActivity7")
} else {
return UIImage(named: "MustachifyUIActivity")
}
}
#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.
func HIPMatchingURLsInActivityItems(activityItems: [AnyObject]) -> [AnyObject] {
return activityItems.filter {
if let url = $0 as? NSURL where !url.fileURL {
return url.pathExtension?.caseInsensitiveCompare("jpg") == .OrderedSame
|| url.pathExtension?.caseInsensitiveCompare("png") == .OrderedSame
}
return false
}
}
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.
override func canPerformWithActivityItems(activityItems: [AnyObject]) -> Bool {
return HIPMatchingURLsInActivityItems(activityItems).count > 0
}
override func prepareWithActivityItems(activityItems: [AnyObject]) {
let HIPMustachifyMeURLFormatString = "http://mustachify.me/%d?src=%@"
if let firstMatch = HIPMatchingURLsInActivityItems(activityItems).first, mustacheType = self.mustacheType {
imageURL = NSURL(string: String(format: HIPMustachifyMeURLFormatString, [mustacheType, firstMatch]))
}
// ...
}
- (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 enumeration:
enum HIPMustacheType: Int {
case English, Horseshoe, Imperial, Chevron, Natural, Handlebar
}
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.
class HIPMustachifyWebViewController: UIViewController, UIWebViewDelegate {
var webView: UIWebView { get }
}
@interface HIPMustachifyWebViewController : UIViewController <UIWebViewDelegate>
@property (readonly, nonatomic, strong) UIWebView *webView;
@end
func activityViewController() -> UIViewController {
let webViewController = HIPMustachifyWebViewController()
let request = NSURLRequest(URL: imageURL)
webViewController.webView.loadRequest(request)
return webViewController
}
- (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
:
let mustacheActivity = HIPMustachifyActivity()
let activityViewController = UIActivityViewController(activityItems: [imageURL], applicationActivities: [mustacheActivity])
HIPMustachifyActivity *mustacheActivity = [[HIPMustachifyActivity alloc] init];
UIActivityViewController *activityViewController =
[[UIActivityViewController alloc] initWithActivityItems:@[imageURL]
applicationActivities:@[mustacheActivity]];
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:
if let URL = NSURL(string: "http://nshipster.com") {
UIApplication.sharedApplication().openURL(URL)
}
NSURL *URL = [NSURL URLWithString:@"http://nshipster.com"];
[[UIApplication sharedApplication] openURL:URL];
System-supported URL schemes include: mailto:
, tel:
, sms:
, and maps:
.
import SafariServices
if let URL = NSURL(string: "http://nshipster.com/uiactivityviewcontroller") {
let _ = try? SSReadingList.defaultReadingList()?.addReadingListItemWithURL(URL,
title: "NSHipster",
previewText: "..."
)
}
@import SafariServices;
NSURL *URL = [NSURL URLWithString:@"http://nshipster.com/uiactivityviewcontroller"];
[[SSReadingList defaultReadingList] addReadingListItemWithURL:URL
title:@"NSHipster"
previewText:@"..."
error:nil];
let image: UIImage = ...
let completionTarget: AnyObject = self
let completionSelector: Selector = "didWriteToSavedPhotosAlbum"
let contextInfo: UnsafeMutablePointer<Void> = nil
UIImageWriteToSavedPhotosAlbum(image, completionTarget, completionSelector, contextInfo)
UIImage *image = ...;
id completionTarget = self;
SEL completionSelector = @selector(didWriteToSavedPhotosAlbum);
void *contextInfo = NULL;
UIImageWriteToSavedPhotosAlbum(image, completionTarget, completionSelector, contextInfo);
import MessageUI
let messageComposeViewController = MFMessageComposeViewController()
messageComposeViewController.messageComposeDelegate = self
messageComposeViewController.recipients = ["mattt@nshipster•com"]
messageComposeViewController.body = "Lorem ipsum dolor sit amet"
navigationController?.presentViewController(messageComposeViewController, animated: true) {
// ...
}
@import MessageUI;
MFMessageComposeViewController *messageComposeViewController = [[MFMessageComposeViewController alloc] init];
messageComposeViewController.messageComposeDelegate = self;
messageComposeViewController.recipients = @[@"mattt@nshipster•com"];
messageComposeViewController.body = @"Lorem ipsum dolor sit amet";
[navigationController presentViewController:messageComposeViewController animated:YES completion:^{
// ...
}];
import MessageUI
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = self
mailComposeViewController.setToRecipients(["mattt@nshipster•com"])
mailComposeViewController.setSubject("Hello")
mailComposeViewController.setMessageBody("Lorem ipsum dolor sit amet", isHTML: false)
navigationController?.presentViewController(mailComposeViewController, animated: true) {
// ...
}
@import MessageUI;
MFMailComposeViewController *mailComposeViewController = [[MFMailComposeViewController alloc] init];
mailComposeViewController.mailComposeDelegate = self;
[mailComposeViewController setToRecipients:@[@"mattt@nshipster•com"]];
[mailComposeViewController setSubject:@"Hello"];
[mailComposeViewController setMessageBody:@"Lorem ipsum dolor sit amet"
isHTML:NO];
[navigationController presentViewController:mailComposeViewController animated:YES completion:^{
// ...
}];
import Social
let tweetComposeViewController = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
tweetComposeViewController.setInitialText("Lorem ipsum dolor sit amet.")
navigationController?.presentViewController(tweetComposeViewController, animated: true) {
// ...
}
@import Social;
SLComposeViewController *tweetComposeViewController = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
[tweetComposeViewController setInitialText:@"Lorem ipsum dolor sit amet."];
[self.navigationController presentViewController:tweetComposeViewController
animated:YES
completion:^{
// ...
}];
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.
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
.