研究 UIActivityViewController

本文翻译自 http://nshipster.com/uiactivityviewcontroller/

原作者:Mattt Thompson

译者:@nixzhu


数据与代码的关系一直都让人好奇。

特定的编程语言,如 LisploMathematica 都是同像性的(homoiconic),意味着它们的代码可作为数据原语呈现,也就是说它们自身就可在代码中被操纵。许多其他语言,包括 Objective-C ,就不同了,它们在这两者之间建立了严格的界限,回避 eval() 和其它潜在的动态指示加载方法,以回避风险。

当问题里的数据过大或难以表示为除了字节流之外的其他东西时,那么代码与数据的这种紧张关系就达到了一个新的高度。关于“如何编码、解码以及解释图像、文档和媒体的二进制表示”的问题从最早的操作系统开始就一直存在着。

OS X 的 Core Services 框架与 iOS 的移动 Core Services 框架都提供函数通过通用类型标识符(Universal Type Identifiers,即UTI)来根据文件扩展和MIME类型识别和分类数据类型。UTI提供了可扩展和可继承的分类系统,它能给予开发人员极大的灵活性,即使是处理最奇特的文件类型。例如,一个 Ruby 源代码文件(.rb)被分类为 Ruby 源代码 > 源代码 > 文本 > 内容 > 数据;一个 QuickTime 电影文件(.mov)被分类为视频 > 电影 > 试听内容 > 内容 > 数据;

在桌面文件系统抽象里,UTI工作得相当好。然而,在一个移动范式里,文件和目录对于用户来说都被隐藏了,于是这很快就失效了。而且,更重要的是,云服务和社交媒体的兴起已经让远程实体比本地文件具有更重要的地位。因此,UTI和URL之间出现了紧张关系。

很明显我们需要其它的某种东西。那 UIActivityViewController 能成为我们拼命追求的解决办法吗?


UIActivityViewController ,出现于 iOS 6,在应用里为分享和操作数据提供了一个统一的服务接口。

给出一个可操作数据的集合,那一个 UIActivityViewController 实例就可如下创建:

  1. NSString *string = ...;
  2. NSURL *URL = ...;
  3. UIActivityViewController *activityViewController =
  4. [[UIActivityViewController alloc] initWithActivityItems:@[string, URL]
  5. applicationActivities:nil];
  6. [navigationController presentViewController:activityViewController
  7. animated:YES
  8. completion:^{
  9. // ...
  10. }];

这将在屏幕的底部呈现如下所示的东西:

UIActivityViewController

默认情况下,UIActivityViewController 将显示所有可用于所提供内容的服务,但我们也可以排除特定的 Activity 类型。

  1. activityViewController.excludedActivityTypes = @[UIActivityTypePostToFacebook];

Activity 类型又分为“操作”和“分享”两大类:

UIActivityCategoryAction

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

UIActivityCategoryShare

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

每个 Activity 类型都支持好多种不同的数据类型。例如,一条 Tweet 可能由 NSString 以及一个附加的图像 和/或 URL 所组成。

不同的 Activity 类型所支持的数据类型





























































































































































Activity 类型

字符串

属性字符串

URL

Data

图像

Asset

其它

发布到 Facebook









发布到 Twitter









发布到 Weibo











信息









✓*

sms:// NSURL

邮件

✓+

✓+

✓+

打印

✓+

✓+

UIPrintPageRenderer,
UIPrintFormatter, &
UIPrintInfo

拷贝到剪贴板







UIColor, NSDictionary

添加到联系人



保存到相机胶卷





添加到阅读列表



发布到 Flickr









发布到 Vimeo







发布到腾讯微博











AirDrop










<UIActivityItemSource> & UIActivityProvider

类似于一个剪贴板条目只在必要时才提供数据,为了避免过多的内存分配或处理时间, Activity 条目可以是自定义类型。

<UIActivityItemSource>

获取数据项

  • activityViewControllerPlaceholderItem:
  • activityViewController:itemForActivityType:

提供数据项信息

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

一个关于这些方法如何使用的例子是自定义一个消息,其根据是否要分享到 Facebook 或 Twitter 分别定义。

  1. - (id)activityViewController:(UIActivityViewController *)activityViewController
  2. itemForActivityType:(NSString *)activityType
  3. {
  4. if ([activityType isEqualToString:UIActivityTypePostToFacebook]) {
  5. return NSLocalizedString(@"Like this!");
  6. } else if ([activityType isEqualToString:UIActivityTypePostToTwitter]) {
  7. return NSLocalizedString(@"Retweet this!");
  8. } else {
  9. return nil;
  10. }
  11. }

创建一个自定义 UIActivity

除了上述系统提供的 Activity ,你也可以自己创建 Activity。

作为例子,让我们创建一个自定义 Activity 类型,它能接受一个图片 URL 并使用 mustache.me 为其安上一瞥胡子。

Jony Ive Before之前

Jony Ive After之后

首先,我们为 Activity 类型定义一个反向DNS标识符(reverse-DNS identifier),指定类别为 UIActivityCategoryAction ,然后提供一个本地化的标题和一个合适于iOS版本的图像:

  1. static NSString * const HIPMustachifyActivityType = @"com.nshipster.activity.Mustachify";
  1. #pragma mark - UIActivity
  2. + (UIActivityCategory)activityCategory {
  3. return UIActivityCategoryAction;
  4. }
  5. - (NSString *)activityType {
  6. return HIPMustachifyActivityType;
  7. }
  8. - (NSString *)activityTitle {
  9. return NSLocalizedString(@"Mustachify", nil);
  10. }
  11. - (UIImage *)activityImage {
  12. if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
  13. return [UIImage imageNamed:@"MustachifyUIActivity7"];
  14. } else {
  15. return [UIImage imageNamed:@"MustachifyUIActivity"];
  16. }
  17. }

接下来,我们创建一个帮助函数,HIPMatchingURLsInActivityItems,它返回一个由任何所支持类型的图像 URL 组成的数组。

  1. static NSArray * HIPMatchingURLsInActivityItems(NSArray *activityItems) {
  2. return [activityItems filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:
  3. ^BOOL(id item, __unused NSDictionary *bindings) {
  4. if ([item isKindOfClass:[NSURL class]] &&
  5. ![(NSURL *)item isFileURL]) {
  6. return [[(NSURL *)item pathExtension] caseInsensitiveCompare:@"jpg"] == NSOrderedSame ||
  7. [[(NSURL *)item pathExtension] caseInsensitiveCompare:@"png"] == NSOrderedSame;
  8. }
  9. return NO;
  10. }]];
  11. }

这个函数用于 -canPerformWithActivityItems:prepareWithActivityItems: 以取得第一个 PNG 或 JPEG 的加了胡子的图像 URL,如果有的话。

  1. - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
  2. return [HIPMatchingURLsInActivityItems(activityItems) count] > 0;
  3. }
  4. - (void)prepareWithActivityItems:(NSArray *)activityItems {
  5. static NSString * const HIPMustachifyMeURLFormatString = @"http://mustachify.me/%d?src=%@";
  6. self.imageURL = [NSURL URLWithString:[NSString stringWithFormat:HIPMustachifyMeURLFormatString, self.mustacheType, [HIPMatchingURLsInActivityItems(activityItems) firstObject]]];
  7. }

我们的网络服务提供了好几种胡子选项,它们定义在一个 NS_ENUM 中:

  1. typedef NS_ENUM(NSInteger, HIPMustacheType) {
  2. HIPMustacheTypeEnglish,
  3. HIPMustacheTypeHorseshoe,
  4. HIPMustacheTypeImperial,
  5. HIPMustacheTypeChevron,
  6. HIPMustacheTypeNatural,
  7. HIPMustacheTypeHandlebar,
  8. };

最终,我们提供一个 UIViewController 来显示图像。在这个例子里,一个简单的 UIWebView 控制器就够了。

  1. @interface HIPMustachifyWebViewController : UIViewController <UIWebViewDelegate>
  2. @property (readonly, nonatomic, strong) UIWebView *webView;
  3. @end
  1. - (UIViewController *)activityViewController {
  2. HIPMustachifyWebViewController *webViewController = [[HIPMustachifyWebViewController alloc] init];
  3. NSURLRequest *request = [NSURLRequest requestWithURL:self.imageURL];
  4. [webViewController.webView loadRequest:request];
  5. return webViewController;
  6. }

要使用我们全新的胡子 Activity,我们简单地将其传递给一个 UIActivityViewController 的初始化函数即可:

  1. HIPMustachifyActivity *mustacheActivity = [[HIPMustachifyActivity alloc] init];
  2. UIActivityViewController *activityViewController =
  3. [[UIActivityViewController alloc] initWithActivityItems:@[imageURL]
  4. applicationActivities:@[mustacheActivity];

手动调用操作

现在正是回忆起 “UIActivityViewController 允许用户执行它们选择的操作” 的好时机,但当情况需要时,分享依然可以手动调用。

为了完整性,下面就来介绍手动执行这些操作的步骤:

打开 URL

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

系统支持的 URL scheme 包括:mailto://、tel://、sms://、and maps://。

添加到 Safari 阅读列表

  1. @import SafariServices;
  2. NSURL *URL = [NSURL URLWithString:@"http://nshipster.com/uiactivityviewcontroller"];
  3. [[SSReadingList defaultReadingList] addReadingListItemWithURL:URL
  4. title:@"NSHipster"
  5. previewText:@"..."
  6. error:nil];

保存到相册

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

发送短信

  1. @import MessageUI;
  2. MFMessageComposeViewController *messageComposeViewController = [[MFMessageComposeViewController alloc] init];
  3. messageComposeViewController.delegate = self;
  4. messageComposeViewController.recipients = @[@"mattt@nshipster•com"];
  5. messageComposeViewController.body = @"Lorem ipsum dolor sit amet";
  6. [navigationController presentViewController:messageComposeViewController animated:YES completion:^{
  7. // ...
  8. }];

发送邮件

  1. @import MessageUI;
  2. MFMailComposeViewController *mailComposeViewController = [[MFMailComposeViewController alloc] init];
  3. [mailComposeViewController setToRecipients:@[@"mattt@nshipster•com"]];
  4. [mailComposeViewController setSubject:@"Hello"];
  5. [mailComposeViewController setMessageBody:@"Lorem ipsum dolor sit amet"
  6. isHTML:NO];
  7. [navigationController presentViewController:mailComposeViewController animated:YES completion:^{
  8. // ...
  9. }];

发送推文

  1. @import Twitter;
  2. TWTweetComposeViewController *tweetComposeViewController =
  3. [[TWTweetComposeViewController alloc] init];
  4. [tweetComposeViewController setInitialText:@"Lorem ipsum dolor sit amet."];
  5. [self.navigationController presentViewController:tweetComposeViewController
  6. animated:YES
  7. completion:^{
  8. //...
  9. }];

IntentKit

虽然所有这些都让人影响深刻也很有用,但在与 Android 上的丰富的 Intent 模型对比之下,iOS 的 Activity 范式中还是有一些特殊的缺失。

在 Android 上,应用可以注册不同的 Intent,以表面它们可用于地图或作为浏览器,而且能被选择为相关 Activity 的默认应用,例如导航或将某个URL加入书签。

虽然 iOS 缺少可扩展的基础架构来支持这些,但一个第三方的库,叫做 IntentKit,由 @lazerwalker (有着 f*ingblocksyntax.com 的声誉) 编写,它是一个有趣的关于我们如何缩小差距的例子。

IntentKit

正常情况下,在一开始一个开发者就要做许多工作,例如查询某个特定的应用是否已被安装,以及构造一个 URL 以支持某个特定的 Activity 等。

IntentKit 合并了连接到这些最流行的服务(如 Web、地图、邮件、Twitter、Facebook以及Google+这些客户端)的逻辑,而且它的 UI 非常类似 UIActivityViewController。

任何想将其应用的分享体验提高一个层次的人都应该研究一下它。

还有一个要发出的有力争论是,关于 iOS 平台的长期生存能力取决于如 UIActivityViewController 这样的分享机制。俗话说,“信息需要自由”。而任何阻挡人民联盟的事物终将输给那些不阻挡联盟的事物。

公开的 远程视图控制器(remote view controller) API 的未来前景给了我关于iOS上分享的未来希望。虽然对现在来说,我们当然可以做得比 UIActivityViewController 更差。(译者注:是这个意思吗? For now, though, we could certainly do much worse than UIActivityViewController.)


欢迎转载,但请一定注明出处! https://github.com/nixzhu/dev-blog