iOS App Extensions之Share Extension

iOS App Extensions之Share Extension,第1张

iOS App Extensions之Share Extension 创建Share Extension扩展配置Share Extension分析ShareViewController自定义分享界面数据共享跳转到容器App不显示界面直接跳转到容器AppDemo遗留问题问题一:点击分享不响应问题问题二:收到的路径数据存在非UTF-8字符 参考文章

创建Share Extension扩展 在原有的项目里,添加扩展target
然后选择”iOS” -> “Application Extension” -> “Share Extension”,点击“Next”。如图:

给扩展起个名字,这里填写了“ShareExtension”,确定你的目标Target,点击“Finish”。


4、创建后工程目录会出现ShareExtension的文件夹。

5、这时候可以选择host app编译运行,看看效果。
这里选择相册做为宿主app。

编译成功后选择一张图片进行分享,d出系统分享面板,发现没有出现你的应该App,别着急,检查一下。

扩展支持版本跟手机版本对不对的上。
可能是你的扩展不支持该分享的文件类型
你需要在扩展的info.plist文件添规则。
<key>NSExtensionAttributes</key>
		<dict>
			<key>NSExtensionActivationRule</key>
			<dict>
				<key>NSExtensionActivationSupportsAttachmentsWithMaxCount</key>
				<string>9</string>
				<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
				<string>1</string>
				<key>NSExtensionActivationSupportsFileWithMaxCount</key>
				<string>1</string>
				<key>NSExtensionActivationSupportsImageWithMaxCount</key>
				<integer>9</integer>
				<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
				<integer>1</integer>
				<key>NSExtensionActivationSupportsText</key>
				<true/>
				<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
				<integer>1</integer>
			</dict>
		</dict>

看出现了:

点击app出现默认的界面:

配置Share Extension

刚创建的扩展中的info.plist内容配置是这样的:

NSExtension : 扩展描述字段,用于描述扩展的属性、设置等。作为一个扩展项目必须要包含此字段。
NSExtensionAttributes : 扩展属性集合字段。用于描述扩展的属性。
NSExtensionMainStoryboard : 设置主界面的Storyboard,如果不想使用storyboard,也可以使用NSExtensionPrincipalClass指定自定义UIViewController子类名
NSExtensionPointIdentifier : 扩展标识,在分享扩展中为:com.apple.share-services。
NSExtensionPrincipalClass : 自定义UI的控制器名称。
NSExtensionActivationSupportsAttachmentsWithMaxCount : 附件最多限制,为数值类型。附件包括File、Image和Movie三大类,单一、混选总量不超过指定数量。
NSExtensionActivationSupportsAttachmentsWithMinCount : 附件最少限制,为数值类型。当设置NSExtensionActivationSupportsAttachmentsWithMaxCount时生效,默认至少选择1个附件,分享菜单中才显示扩展插件图标。
NSExtensionActivationSupportsFileWithMaxCount : 文件最多限制,为数值类型。文件泛指除Image/Movie之外的附件,例如【邮件】附件、【语音备忘录】等。单一、混选均不超过指定数量。
NSExtensionActivationSupportsImageWithMaxCount : 图片最多限制,为数值类型。单一、混选均不超过指定数量。
NSExtensionActivationSupportsMovieWithMaxCount : 视频最多限制,为数值类型。单一、混选均不超过指定数量
NSExtensionActivationSupportsText : 是否支持文本类型,布尔类型,默认不支持。如【备忘录】的分享
NSExtensionActivationSupportsWebURLWithMaxCount : Web链接最多限制,为数值类型。默认不支持分享超链接,需要自己设置一个数值。
NSExtensionActivationSupportsWebPageWithMaxCount : Web页面最多限制,为数值类型。默认不支持Web页面分享,需要自己设置一个数值。

对于不同的应用里面有可能出现只允许接受某种类型的内容,那么Share Extension就不能一直出现在分享菜单中,因为不同的应用提供的分享内容不一样,这就需要通过设置NSExtensionActivationRule字段来决定Share Extension是否显示。

将NSExtensionActivationRule字段类型由String改为Dictionary。展开NSExtensionActivationRule字段,创建其子项NSExtensionActivationSupportsImageWithMaxCount(支持分享图片类型),并设置一个限制数量。

具体可查看官方文档App Extension Keys

分析ShareViewController

ShareViewController继承SLComposeServiceViewController里面默认配置了三个方法:

isContentValid: 来判断我们获取到得数据是否是我们想要的.
didSelectPost: 是惦记post按钮后触发选择的方法。
configurationItems:返回一个配置项数组,会在默认界面底下添加一个tableView列表,可以进行点击选择,也可以在跳转到其他页面选择内容。
例如:

- (NSArray *)configurationItems {
    // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
    
    __weak typeof(self) weakSelf = self;
    
    SLComposeSheetConfigurationItem *item1 = [[SLComposeSheetConfigurationItem alloc] init];
    item1.title = @"发送给朋友";
    item1.value = @"请选择";
    item1.tapHandler = ^{
        NSLog(@"发送给朋友");
        [self validateContent];
    };
    
    SLComposeSheetConfigurationItem *item2 = [[SLComposeSheetConfigurationItem alloc] init];
    item2.title = @"发送到朋友圈";
    item2.value = @"点我";
    item2.tapHandler = ^{
        NSLog(@"发送到朋友圈");
        ShareActViewController *vc = [ShareActViewController new];
        [weakSelf pushConfigurationViewController:vc];
        
        vc.clickBlock = ^(NSString * _Nonnull text) {
            [weakSelf popConfigurationViewController];
            weakSelf.textView.text = text;
        };
    };
    
    return @[item1, item2];
}


输入文本后,点击post会触发方法didSelectPost,通过contentText属性获取输入的文本,以及self.extensionContext.inputItems获取分享到文件。


NSLog(@"share 文本:%@", self.contentText);
    
    NSExtensionItem *imageItem = [self.extensionContext.inputItems firstObject];
    NSItemProvider *imageItemProvider = [[imageItem attachments] firstObject];

    if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeFileURL]) {

        [imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeFileURL options:nil completionHandler:^(__kindof id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {
            
            NSLog(@"收到的文件 : %@",item);
            if ([(NSObject *)item isKindOfClass:[NSURL class]]) {
                NSURL *url = (NSURL *)item;
                
                /// 保存文件数据
                NSData *data = [NSData dataWithContentsOfURL:url];
                [KSuiteUserDefault(kSuiteShareName) setObject:data forKey:kShareFileData];
                [KSuiteUserDefault(kSuiteShareName) synchronize];

                /// 跳转到容器app
                UIResponder* responder = self;
                while ((responder = [responder nextResponder]) != nil)
                {
                    NSLog(@"responder = %@", responder);
                    if([responder respondsToSelector:@selector(openURL:)] == YES)
                    {
                        [responder performSelector:@selector(openURL:) withObject:[NSURL URLWithString:[NSString stringWithFormat:@"extensionShare://%@",[url absoluteString]]]];
                    }
                }
            }
        }];

    }
    /// 关闭视图界面
    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];

自定义分享界面 你需要创建一个继承于ViewController新的控制器,例如:ShareCustomViewController。文件夹中默认创建的ShareViewControllerstoryboard可以删掉。修改Info.plist文件的配置。删除掉NSExtensionMainStoryboard项,添加NSExtensionPrincipalClass,值是新创建的控制器。
clean后再编译一下。编译成功后你就可以自定义UI了。
- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor orangeColor];
    
    UIView *container = [[UIView alloc] initWithFrame:CGRectMake((self.view.frame.size.width - 300) / 2, (self.view.frame.size.height - 175) / 2, 300, 175)];
        container.layer.cornerRadius = 7;
        container.layer.borderColor = [UIColor lightGrayColor].CGColor;
        container.layer.borderWidth = 1;
        container.layer.masksToBounds = YES;
        container.backgroundColor = [UIColor whiteColor];
        container.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
        [self.view addSubview:container];

        //定义Post和Cancel按钮
        UIButton *cancelBtn = [UIButton buttonWithType:UIButtonTypeSystem];
        [cancelBtn setTitle:@"Cancel" forState:UIControlStateNormal];
        cancelBtn.frame = CGRectMake(8, 8, 65, 40);
        [cancelBtn addTarget:self action:@selector(cancelBtnClickHandler:) forControlEvents:UIControlEventTouchUpInside];
        [container addSubview:cancelBtn];

        UIButton *postBtn = [UIButton buttonWithType:UIButtonTypeSystem];
        [postBtn setTitle:@"Post" forState:UIControlStateNormal];
        postBtn.frame = CGRectMake(container.frame.size.width - 8 - 65, 8, 65, 40);
        [postBtn addTarget:self action:@selector(postBtnClickHandler:) forControlEvents:UIControlEventTouchUpInside];
        [container addSubview:postBtn];

        //定义一个分享链接标签
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(8,
                                                                   cancelBtn.frame.origin.y + cancelBtn.frame.size.height + 8,
                                                                   container.frame.size.width - 16,
                                                                   container.frame.size.height - 16 - cancelBtn.frame.origin.y - cancelBtn.frame.size.height)];
        label.numberOfLines = 0;
        label.textAlignment = NSTextAlignmentCenter;
        [container addSubview:label];

        //获取分享链接
        __block BOOL hasGetUrl = NO;
        [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

            [obj.attachments enumerateObjectsUsingBlock:^(NSItemProvider *  _Nonnull itemProvider, NSUInteger idx, BOOL * _Nonnull stop) {

                if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"])
                {
                    [itemProvider loadItemForTypeIdentifier:@"public.url" options:nil completionHandler:^(id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {

                        if ([(NSObject *)item isKindOfClass:[NSURL class]])
                        {
                            dispatch_async(dispatch_get_main_queue(), ^{

                                label.text = ((NSURL *)item).absoluteString;
                                self.urlString = ((NSURL *)item).absoluteString;
                            });
                        }
                    }];

                    hasGetUrl = YES;
                    *stop = YES;
                }

                *stop = hasGetUrl;

            }];

        }];
}


- (void)cancelBtnClickHandler:(id)sender
{
    //取消分享
    [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"CustomShareError" code:NSUserCancelledError userInfo:nil]];
}

- (void)postBtnClickHandler:(id)sender
{
    UIResponder* responder = self;
    while ((responder = [responder nextResponder]) != nil)
    {
        NSLog(@"responder = %@", responder);
        if([responder respondsToSelector:@selector(openURL:)] == YES)
        {
//            [responder performSelector:@selector(openURL:) withObject:[NSURL URLWithString:@"extensionShare://"]];
            [responder performSelector:@selector(openURL:) withObject:[NSURL URLWithString:[NSString stringWithFormat:@"extensionShare://%@",self.urlString]]];
        }
    }
    
    //执行分享内容处理
    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
}
数据共享

数据共享的配置和三种方式,具体参考上一篇文章:
iOS App Extensions初识及工作原理

跳转到容器App

首先你需要在App里配置UrlScheme

由于扩展中是无法访问sharedApplication对象,因此不能使用openUrl进行程序间的跳转,那怎么办呢。
也许你查看了一下self.extensionContext属性,发现了这个API:

// Asks the host to open a URL on the extension's behalf
- (void)openURL:(NSURL *)URL completionHandler:(void (^ _Nullable)(BOOL success))completionHandler;

然后试了一下发现没有效果,不好意思这个API只对today widget小组件管用。

那怎么办呢?只能根据响应链获取可以触发openURL:的对象,执行跳转:

UIResponder* responder = self;
while ((responder = [responder nextResponder]) != nil) {
  NSLog(@"responder = %@", responder);
  if([responder respondsToSelector:@selector(openURL:)] == YES) {
      [responder performSelector:@selector(openURL:) withObject:[NSURL URLWithString:@"extensionShare://"]];
   }
}

跳转后触发AppDelegateapplication:openURL:options:函数,在这里进行数据的处理。

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    NSLog(@"openURL : %@", url);
    if ([url.description hasPrefix:@"extensionShare"]) {
        NSLog(@"分享跳转");
    }
    return YES;
}
不显示界面直接跳转到容器App

如果你想点击的时候不显示界面直接跳转到容器App,那么你需要创建一个自定义控制器,然后在viewWillAppear视图出现的时候接收到文件后跳转并关闭界面。

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    NSExtensionItem *item = [self.extensionContext.inputItems firstObject];
    NSItemProvider *itemProvider = [[item attachments] firstObject];

    if([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeData]) {

        [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(__kindof id<NSSecureCoding>  _Nullable item, NSError * _Null_unspecified error) {
            
            NSLog(@"收到的文件 : %@",item);
            if ([(NSObject *)item isKindOfClass:[NSURL class]]) {
                NSURL *url = (NSURL *)item;
                                
                [self.shareHandler saveFileWithUrl:url];
                
                UIResponder* responder = self;
                while ((responder = [responder nextResponder]) != nil)
                {
                    NSLog(@"responder = %@", responder);
                    if([responder respondsToSelector:@selector(openURL:)] == YES)
                    {
                       [responder performSelector:@selector(openURL:) withObject:[NSURL URLWithString:@"extensionShare://"]];
                    }
                }
                
                [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
            }
        }];
    }
}
Demo

demo

遗留问题 问题一:点击分享不响应问题

App中添加了Share Extension,又配置了Document Type,d出来的分享面板出现了拷贝到xxx app,和xxx app两个应用入口,点击拷贝到xxx app后成功分享,再点击xxx app发现不响应了。
目前不知道该如何解决,只能关闭Document Type的配置,但这样隔空投送过来的文件就无法在app打开。
有遇到这个问题的兄弟们帮忙留个言。

问题二:收到的路径数据存在非UTF-8字符

正常的文件传输收到的数据类型基本上是NSURL的文件路径格式,而类似于分享.db的文件类型,收到的数据类型是NSData的路径格式。
这时候怎么办呢,第一想法肯定是采用datastring获取文件路径,思路是没错的:

[[NSString alloc] initWithData:data] encoding:NSUTF8StringEncoding];

可是返回的是nil。这是因为data中存在非UTF8编码格式的字符。
暂时的解决办法参照编码转化
如有更好的解决办法可以留言下,万分感激!

参考文章

iOS App Extensions初识及工作原理
iOS App Extensions之Action Extension

https://www.cnblogs.com/junhuawang/p/8182868.html
https://www.jianshu.com/p/01c933254e66

https://stackoverflow.com/questions/24297273/openurl-not-work-in-action-extension/28037297#28037297
https://www.thinbug.com/q/56438916#google_vignette
https://developer.apple.com/forums/thread/65621

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/994094.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存