Share Extension

参考文档:

  1. https://www.jianshu.com/p/863ce6729455
  2. App Extension Programming Guide

1.创建一个Share Extension

Extension不能单独创建必须依赖于应用工程项目,这个应用被称为Containing App(容器App)

  • 创建Share Extension File->New->Target 然后会出现以下提示图,选择"Activate"至此,创建完成!

项目中多出以下内容:

.plist中有一个扩展标识:

2.运行Share Extension


选择taget然后选择提示框中的app来运行,因为是ShareExtension在其他App点击分享按钮并选中时就是运行ShareExtension的代码。这个其他App被称为Host App(宿主App)

若是没看到自己的扩展就点击旁边的More然后激活自己的扩展

  • 扩展显示的名称可以在.plist文件的CFBundleDisplayName键的值进行修改。 An app extension needs a short, recognizable name that includes the name of the containing app, using the pattern < Containing app name >—< App extension name>. This makes it easier for users to manage extensions throughout the system. You can, optionally, use the containing app’s name as-is for your extension, in the common case that your containing app provides exactly one extension.

就会弹出

3.有时项目会有要求只能分享图片或者URL的需求可以通过配置info.plist(App Extension Keys官方详情文档)

  • 设置分享数据类型

    • 将info.plist的NSExtensionActivationRule字段修改成NSDictionary
    • 根据需求设置分享类型已设置分享图片为例添加NSExtensionActivationSupportsImageWithMaxCount,并设置一个最大限制字数如下(如果查出限制数量的话,扩展会从选择行中徐傲视):(分享类型官方详情文档)

4.分享界面(本身)

Share Extension的分享界面即为下方的ShareViewController继承SLComposeServiceViewController控制器
分享界面:可以看出是由Cancel、Post按钮、文本编辑框和放置图片(附件)组成

  • 文本编辑框属性
// SLComposeServiceViewController.h
// The textView displaying the (editable) text content on the sheet. When it's time to post, grab self.textView.text.
// SLComposeServiceViewController creates textView in -loadView and sets itself to be the textView's delegate.
#if TARGET_OS_IPHONE
@property (readonly, nonatomic) UITextView *textView;
#else
@property (readonly) NSTextView *textView;
#endif

// Convenience. This returns the current text from the textView.
@property (readonly, NS_NONATOMIC_IOSONLY) NSString *contentText; 

// When textView is empty, this string is displayed instead.
// NOTE: This uses the iOS naming convention: "placeholder" vs. "placeholderString" on OS X.
@property (copy, NS_NONATOMIC_IOSONLY) NSString *placeholder;
  • -isContentValid 判断当前内容是否符合条件可以post和监听文本编辑
- (BOOL)isContentValid {
    // Do validation of contentText and/or NSExtensionContext attachments here
    // 可以判断当前分享内容是否符合我们的要求,比如编辑的文本是否含有敏感字眼
    if ([self.contentText containsString:@"我是敏感字眼"]) {
        return NO;  // 当前post按钮会处于不可用状态
    }
    return YES;
}

  • Cancle、Post按钮点击事件
 /*
 Posting & Canceling
 */
 // 点击Post触发事件
 // The default implementation calls the extensionContext's -completeRequestReturningItems:completionHandler: method with nil.
 // 子类必须实现这个方法,可以调用super方法不然的话实现上面的方法
 - (void)didSelectPost;

 // 点击Cancel触发事件可以重写自定义
 // 默认实现调用extensionContext -cancelRequestWithError:NSError / NSUserCancelledError方法。
 - (void)didSelectCancel;

 // 点击Cancel触发事件然后在触发- didSelectCancel方法
 // 子类不需要重写
 - (void)cancel;

5.自定义分享界面

  • 自定义一个视图控制器 MyShareExtensionViewControll
  • 在info.plist中进行配置: 添加NSExtensionPrincipalClass对应自定义视图控制器类名; 删掉NSExtensionMainStoryboard Key对应着MainInterface;
  • 在MyShareExtensionViewControll中根据具体需求进行设置
- (void)initShareView {
    CGFloat bgViewWidth = SCREENWIDTH*0.6;
    CGFloat bgViewHeight = bgViewWidth*1.25;
    // 背景View
    UIView *bgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, bgViewWidth, bgViewHeight)];
    bgView.center = CGPointMake(SCREENWIDTH/2, SCREENHEIGHT/2);
    bgView.backgroundColor = [UIColor grayColor];
    [self.view addSubview:bgView];

    CGFloat buttonWidth = bgViewWidth/4;
    CGFloat buttonHeight = 20;
    // 取消按钮
    UIButton *cancleButton = [[UIButton alloc] initWithFrame:CGRectMake(10, 10, buttonWidth, buttonHeight)];
    [cancleButton setTitle:@"取消" forState:UIControlStateNormal];
    [cancleButton addTarget:self action:@selector(cancelButtonAction:) forControlEvents:UIControlEventTouchUpInside];
    [bgView addSubview:cancleButton];

    // post按钮
    UIButton *postButton = [[UIButton alloc] initWithFrame:CGRectMake(bgViewWidth-10-buttonWidth, 10, buttonWidth, buttonHeight)];
    [postButton setTitle:@"发送" forState:UIControlStateNormal];
    [postButton addTarget:self action:@selector(postButtonAction:) forControlEvents:UIControlEventTouchUpInside];
    [bgView addSubview:postButton];

    self.bgView = bgView;
    [self initAttachImageView];

}
- (void)initAttachImageView {
    // 展示分享的图片
    NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
    NSDictionary *dic = item.userInfo;
    NSArray *providerArray = dic[NSExtensionItemAttachmentsKey];
    NSItemProvider *provider = providerArray[0];
    if ([provider hasItemConformingToTypeIdentifier:@"public.image"]) {
        [provider loadItemForTypeIdentifier:@"public.image" options:nil completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {
            NSData *imageData = [NSData dataWithContentsOfURL:(NSURL *)item];
            if (imageData) {
                UIImage *image = [UIImage imageWithData:imageData];
                // 图片imageView
                UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
                imageView.frame = CGRectMake(0, 50, self.bgView.frame.size.width, (self.bgView.frame.size.width*image.size.height)/image.size.width);
                [self.bgView addSubview:imageView];
            }

        }];
    }

}
- (void)cancelButtonAction:(UIButton *)button {
    [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"CustomShareError" code:NSUserCancelledError userInfo:nil]];
}
- (void)postButtonAction:(UIButton *)button {

    // 处理
    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
}

如图:

6.获取数据

  • 附件
 //  SLComposeServiceViewController.h
 /*
 Sheet Content

 NOTE: Attachments (e.g., links, images, videos) and initial text are accessible via the view controller's extensionContext.
 */
 // 可知要获取分享的连接、图片、视频等可以从extensionContext中实现
  • NSExtensionContext(扩展上下文)
// Returns the extension context. Also acts as a convenience method for a view controller to check if it participating in an extension request.
@property (nullable, nonatomic,readonly,strong) NSExtensionContext *extensionContext NS_AVAILABLE_IOS(8_0);

// UIViewController控制器在iOS8.0之后都有这个属性,非扩展请求获取到的extensionContext为nil,可用来判断是否参与扩展请求
  • NSExtensionContext
// 1.获取扩展内容
// The list of input NSExtensionItems associated with the context. If the context has no input items, this array will be empty.
@property(readonly, copy, NS_NONATOMIC_IOSONLY) NSArray *inputItems;   // 可知数组中存放的是NSExtensionItem

// 2.有一个key
// Key in userInfo. Value is a dictionary of NSExtensionItems and associated NSError instances.是NSExtensionItem的userInfo(NSDictionary *)的中错误信息对应的Key
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemsAndErrorsKey API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

// 3.有的需求是在扩展中打开Containing App)(或者是打开一个URL)
> 官方文档中说明Today 扩展才可以,并且在提交审核的时候要说明
// Asks the host to open an URL on the extension's behalf
- (void)openURL:(NSURL *)URL completionHandler:(void (^ _Nullable)(BOOL success))completionHandler;

// 4.定义了关于Host App的四个通知
// Host App将要进入
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostWillEnterForegroundNotification API_AVAILABLE(ios(8.2), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);

// Host App已经进入后台
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostDidEnterBackgroundNotification API_AVAILABLE(ios(8.2), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);

// The host process will resign active status (stop receiving events), the extension may be suspended.Host App将要被挂起
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostWillResignActiveNotification API_AVAILABLE(ios(8.2), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);

// The host process did become active (begin receiving events).Host App已经被激活
FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostDidBecomeActiveNotification API_AVAILABLE(ios(8.2), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
  • NSExtensionItem
 // (optional) title for the item
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSAttributedString *attributedTitle;

// (optional) content text
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSAttributedString *attributedContentText;

// (optional) Contains images, videos, URLs, etc. This is not meant to be an array of alternate data formats/types, but instead a collection to include in a social media post for example. These items are always typed NSItemProvider.
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSArray *attachments;  // 扩展中分享的内容(附件)就存放在attachments,这些附件类型为NSItemProvider

// (optional) dictionary of key-value data. The key/value pairs accepted by the service are expected to be specified in the extension's Info.plist. The values of NSExtensionItem's properties will be reflected into the dictionary.
@property(nullable, copy, NS_NONATOMIC_IOSONLY) NSDictionary *userInfo;  // NSExtensionItem的上面三个属性值和key都会在userInfo一一映射
// userInfo的key
FOUNDATION_EXTERN NSString * _Null_unspecified const NSExtensionItemAttributedTitleKey API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
FOUNDATION_EXTERN NSString * _Null_unspecified const NSExtensionItemAttributedContentTextKey API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
FOUNDATION_EXTERN NSString * _Null_unspecified const NSExtensionItemAttachmentsKey API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
  • NSItemProvider获取到了附件,对不同的附件可能有不同处理,所以我们可以判断附件类型
// 1.判断附件是否存在某种类型的item
// Returns YES if the item provider has at least one item that conforms to the supplied type identifier.
- (BOOL)hasItemConformingToTypeIdentifier:(NSString *)typeIdentifier; // 判断是否含有指定typeIdentifier类型的item存在,返回BOOL

// 2.加载item
- (void)loadItemForTypeIdentifier:(NSString *)typeIdentifier options:(nullable NSDictionary *)options completionHandler:(nullable NSItemProviderCompletionHandler)completionHandler;
  • 获取图片例子(其他类型一样)
    __weak typeof(self) weakSelf = self;
    NSExtensionItem *extension = self.extensionContext.inputItems.firstObject;
    NSArray *providerArray = extension.userInfo[NSExtensionItemAttachmentsKey];
    [providerArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSItemProvider *provider = providerArray[0];
        if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {  // 判断是否含有图片类型的provider
            [provider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {  
                NSData *imageData = [[NSData alloc] initWithContentsOfURL:(NSURL *)item];  // 选中的data
            }];
        }
    }];

7.Extension和Containing App数据共享

iOS中应用不允许应用与应用直接进行数据的交互,提供给了App Groups来进行数据交互,App Groups可以通过NSUserDefaultsUse(Core Data, SQLite, or Posix locks to help coordinate data access in a shared container.)来进行相互的数据交互.

  • 先激活App Groups服务
    Containging App激活操作如下

    • 开发者后台创建App ID,打开App Groups
    • 创建App Groups

      勾选上创建的group

    • 扩展激活也是一样的只不过不需要创建新的app group,勾选和Containing App同一个group就行

  • 数据交互操作

    • NSUserDefaults
       // 1.存入 
       // 初始化一个给App Groups使用的NSUserDefaults,一定要用initWithSuiteName:方法,取出也是. SuiteName就是刚才勾选的group
       NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.xxx"];
      [defaults setObject:<#(nullable id)#> forKey:<#(nonnull NSString *)#>];
      [defaults synchronize];
    
      // 2.取出
      NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.xxx"];
      id data = [defaults objectForKey:<#(nonnull NSString *)#>];
    
    • NSFileManager
     // 1.存入
     // 打开指定group的目录
     NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.xxx"];
     NSURL *imageURL = [groupURL URLByAppendingPathComponent:@"image"];
     [data writeToURL:imageURL atomically:YES];
    
     // 2.取出
     // 打开指定group的目录
      NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.xxx"];
      NSURL *imageURL = [groupURL URLByAppendingPathComponent:@"image"];
      NSData *data = [NSData dataWithContentsOfURL:<#(nonnull NSURL *)#>];
    

// 继续研究在扩展中数据的上传和下载怎样处理

results matching ""

    No results matching ""