【iOS扩展开发攻略】Share Extension

作者: 杰嗒嗒的阿杰 | 来源:发表于2016-09-10 16:43 被阅读13855次

    1. 什么是扩展?

    扩展( Extension )是 iOS 8 中引入的一个非常重要的新特性。扩展让 app 之间的数据交互成为可能。用户可以在 app 中使用其他应用提供的功能,而无需离开当前的应用。在 iOS 8 系统之前,每一个 app 在物理上都是彼此独立的, app 之间不能互访彼此的私有数据。而在引入扩展之后,其他 app 可以与扩展进行数据交换。基于安全和性能的考虑,每一个扩展运行在一个单独的进程中,它拥有自己的 bundle , bundle 后缀名是.appex 。扩展 bundle 必须包含在一个普通应用的 bundle 的内部。

    iOS 8 系统有 6 个支持扩展的系统区域,分别是 Today 、 Share 、 Action 、 Photo Editing 、 Storage Provider 、 Custom keyboard 。支持扩展的系统区域也被称为扩展点。

    Today Widget

    对于赛事比分,股票、天气、快递这类需要实时获取的信息,可以在通知中心的Today 视图中创建一个 Today 扩展实现。 Today 扩展又称为 Widget 。

    Today扩展效果图

    Share

    在 iOS 8 之前,用户只有 Facebook,Twitter 等有限的几个分享选项可以选择。如果希望将内容分享到 Pinterest ,开发者则需要一些额外的努力。在 iOS 8 中,开发者可以创建自定义的分享选项。

    Share扩展效果图

    Action

    action 在所有支持的扩展点中扩展性最强的一个。它可以实现转换另一个 app 上下文中的内容。苹果在 WWDC 大会上演示了一个 Bing 翻译动作扩展,它可以将在 Safari 中选中的文本翻译成不同的语言。

    Action扩展效果图

    Photo Editing

    在 iOS 8 之前,如果你想为你的照片添加一个特殊的滤镜,你需要进入第三方 app 中,这个过程是相当繁琐的。在 iOS 8 中,你可以直接在 Photos 中使用第三方 app ,如 Instagram , VSCO cam 、 Aviary 提供的 Photo Editing 扩展完成对图片的编辑,而无需离开当前的 app 。

    Photo Editing扩展效果图

    Storage Provider

    Storage Provider 让跨多个文件存储服务之间的管理变得更简单。类似 Dropbox 、 Google Drive 等存储提供商通过在 iOS 8 中提供一个 Storage Provider 扩展, app 直接可以使用这些扩展检索和存储文件而不再需要创建不必要的拷贝。

    Storage Provider扩展效果图

    Custom Keyboard

    苹果公司在 2007 年率先推出了触摸屏键盘,但一直没多大改进。在这一方面, Android 则将键盘权限开放给了第三方开发者,所以出现了许多像 Swype , SwiftKey 等优秀的键盘输入法。在 iOS 8 中,苹果终于将键盘权限开发给了第三方开发者,自定义键盘输入法可以让用户在整个系统范围内使用。

    Custom Keyboard扩展效果图

    以下是iOS 9中新增扩展

    网络扩展

    开发者可以通过改扩展来实现自定义的VPN客户端、透明的网络代理客户端以及实现动态的设备端网络内容过滤。

    Safari扩展

    该扩展可以让用户通过Safari的分享链接看到你的内容。又或者提供一个屏蔽列表,让你的用户使用你的App浏览Web内容时屏蔽指定的内容。

    Spotlight扩展

    该扩展可以对App内的数据进行索引,并且可以在不重启App的情况下重建数据索引。

    Audio Unit扩展

    该扩展允许App提供类似于GarageBand,Logic等App提供的乐器演奏,音频特效,声音合成功能。

    2. 转入正题 - Share Extension

    本篇文章主要是探讨Share Extension的开发与使用。下面会结合一个例子对其做一个全面的探讨和深入的了解。

    2.1 创建Share Extension扩展Target

    ** 注:扩展不能单独创建,必须依赖于应用工程项目,因此如果你还没有创建一个应用工程,先去创建一个。**

    1、打开项目设置,在TARGETS侧栏地下点击“+”号来创建一个新的Target,如图:

    添加Target

    2、然后选择”iOS” -> “Application Extension” -> “Share Extension”,点击“Next”。如图:

    创建Share扩展

    3、给扩展起个名字,这里填写了“Share”,点击“Finish”。如图:

    填写扩展信息

    4、这时候会提示创建一个Scheme,点击“Activate”。如图:

    那么,直到这里创建Share Extension的工作就算是完成了。接下来可以先进行一下编译运行。这里跟做App开发的时候会稍微有点不一样。因为Extension是需要Host App(宿主应用)来运行的。所以,XCode中会弹出界面让我们选择一个iOS的App来运行Extension。如图:

    选择宿主应用

    这里我选择了XCode建议的应用Safari,然后点击“Run”来进行调试运行。XCode会启动Safari,如图:

    能看到Safari中间的分享按钮是灰色不可用的。别急,你还没打开一个网页呢_。我们随便点开一个网页,可以看到分享按钮变为激活状态。点击分享按钮就会弹出分享菜单,如图:

    运行效果图

    可以看到刚才建立的Share扩展已经显示在面板上了,如果你没有发现自己的扩展,那么你可以将菜单滑动到最右边,在“更多”选项中激活自己的扩展。如图:

    我们点击自己创建的分享项,其弹出一个分享窗口。如图:

    分享界面效果图

    2.2. 配置Share Extension

    接下来我们需要给他一些设置。我们展开XCode左侧栏的Share目录,找到Info.plist文件。如:

    扩展Info.plist

    我们只需要关注以下几个字段的设置:

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

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

    1. 将NSExtensionActivationRule字段类型由String改为Dictionary。
    2. 展开NSExtensionActivationRule字段,创建其子项NSExtensionActivationSupportsWebURLWithMaxCount,并设置一个限制数量。

    调整后如下图所示:

    Info.plist

    2.3 处理Share Extension中的数据

    其实在Share Extension中默认都会有一个数据展现的UI界面。该界面继承SLComposeServiceViewController这个类型,如:

    @interface ShareViewController : SLComposeServiceViewController
    
    @end
    

    其展现效果,如图:

    分享界面

    顶部包括了标题、取消(Cancel)按钮和提交(Post)按钮。然后下面跟着左边就是一个文本编辑框,右边就是一个图片显示控件。那么,每当用户点击取消按钮或者提交按钮时,都会分别触发下面的方法:

    /**
     *  点击取消按钮
     */
    - (void)didSelectCancel
    {
        [super didSelectCancel];
    }
    
    /**
     *  点击提交按钮
     */
    - (void)didSelectPost
    {
        // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
    
        // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
        [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
    }
    

    在这两个方法里面可以进行一些自定义的操作。一般情况下,当用户点击提交按钮的时候,扩展要做的事情就是要把数据取出来,并且放入一个与Containing App(** 容器程序,尽管苹果开放了Extension,但是在iOS中extension并不能单独存在,要想提交到AppStore,必须将Extension包含在一个App中提交,并且App的实现部分不能为空,这个包含Extension的App就叫Containing app。Extension会随着Containing App的安装而安装,同时随着ContainingApp的卸载而卸载。**)共享的数据介质中(包括NSUserDefault、Sqlite、CoreData),要跟容器程序进行数据交互需要借助AppGroups服务,下面的章节会对这块进行详细说明。下面先来看看怎么获取扩展中的数据。

    在ShareExtension中,UIViewController包含一个extensionContext这样的上下文对象:

    @interface UIViewController(NSExtensionAdditions) <NSExtensionRequestHandling>
    
    // 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);
    
    @end
    

    通过操作它就可以获取到分享的数据,返回宿主应用的界面等操作。我们可以先看一下extensionContext的定义。

    NS_CLASS_AVAILABLE(10_10, 8_0)
    @interface NSExtensionContext : NSObject
    
    // 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;
    
    // Signals the host to complete the app extension request with the supplied result items. The completion handler optionally contains any work which the extension may need to perform after the request has been completed, as a background-priority task. The `expired` parameter will be YES if the system decides to prematurely terminate a previous non-expiration invocation of the completionHandler. Note: calling this method will eventually dismiss the associated view controller.
    - (void)completeRequestReturningItems:(nullable NSArray *)items completionHandler:(void(^ __nullable)(BOOL expired))completionHandler;
    
    // Signals the host to cancel the app extension request, with the supplied error, which should be non-nil. The userInfo of the NSError will contain a key NSExtensionItemsAndErrorsKey which will have as its value a dictionary of NSExtensionItems and associated NSError instances.
    - (void)cancelRequestWithError:(NSError *)error;
    
    // Asks the host to open an URL on the extension's behalf
    - (void)openURL:(NSURL *)URL completionHandler:(void (^ __nullable)(BOOL success))completionHandler;
    
    @end
    
    // Key in userInfo. Value is a dictionary of NSExtensionItems and associated NSError instances.
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemsAndErrorsKey NS_AVAILABLE(10_10, 8_0);
    
    // The host process will enter the foreground
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostWillEnterForegroundNotification NS_AVAILABLE_IOS(8_2);
    
    // The host process did enter the background
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostDidEnterBackgroundNotification NS_AVAILABLE_IOS(8_2);
    
    // The host process will resign active status (stop receiving events), the extension may be suspended
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostWillResignActiveNotification NS_AVAILABLE_IOS(8_2);
    
    // The host process did become active (begin receiving events)
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionHostDidBecomeActiveNotification NS_AVAILABLE_IOS(8_2);
    

    NSExtensionContext的结构比较简单,包含一个属性和三个方法。其说明如下:

    方法 说明
    inputItems 该数组存储着容器应用传入给NSExtensionContext的NSExtensionItem数组。其中每个NSExtensionItem标识了一种类型的数据。要获取数据就要从这个属性入手。
    completeRequestReturningItems:<br />completionHandler: 通知宿主程序的扩展已完成请求。调用此方法后,扩展UI会关闭并返回容器程序中。其中的items就是返回宿主程序的数据项。
    cancelRequestWithError: 通知宿主程序的扩展已取消请求。调用此方法后,扩展UI会关闭并返回容器程序中。其中error为错误的描述信息。
    NSExtensionItemsAndErrorsKey NSExtensionItem的userInfo属性中对应的错误信息键名。

    类的下面还定义了一些通知,这些通知都是跟宿主程序的行为相关,在设计扩展的时候可以根据这些通知来进行对应的操作。其说明如下:

    通知名称 说明
    NSExtensionHostWillEnterForegroundNotification 宿主程序将要返回前台通知
    NSExtensionHostDidEnterBackgroundNotification 宿主程序进入后台通知
    NSExtensionHostWillResignActiveNotification 宿主程序将要被挂起通知
    NSExtensionHostDidBecomeActiveNotification 宿主程序被激活通知

    2.3.1 从inputItems中获取数据

    inputItems是包含NSExtensionItem类型对象的数组。那么,要处理里面的数据还得先来了解一下NSExtensionItem的结构:

    @interface NSExtensionItem : NSObject<NSCopying, NSSecureCoding>
    
    // (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;
    
    // (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;
    
    @end
    
    // Keys corresponding to properties exposed on the NSExtensionItem interface
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemAttributedTitleKey NS_AVAILABLE(10_10, 8_0);
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemAttributedContentTextKey NS_AVAILABLE(10_10, 8_0);
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionItemAttachmentsKey NS_AVAILABLE(10_10, 8_0);
    

    NSExtensionItem包含四个属性

    属性 说明
    attributedTitle 标题。
    attributedContentText 内容。
    attachments 附件数组,包含图片、视频、链接等资源,封装在NSItemProvider类型中。
    userInfo 一个key-value结构的数据。NSExtensionItem中的属性都会在这个属性中一一映射。

    对应userInfo结构中的NSExtensionItem属性的键名如下:

    名称 说明
    NSExtensionItemAttributedTitleKey 标题的键名
    NSExtensionItemAttributedContentTextKey 内容的键名
    NSExtensionItemAttachmentsKey 附件的键名

    从上面的定义可以看出除了文本内容,其他类型的内容都是作为附件存储的,而附件又是封装在一个叫NSItemProvider的类型中,其定义如下:

    typedef void (^NSItemProviderCompletionHandler)(__nullable id <NSSecureCoding> item, NSError * __null_unspecified error);
    typedef void (^NSItemProviderLoadHandler)(__null_unspecified NSItemProviderCompletionHandler completionHandler, __null_unspecified Class expectedValueClass, NSDictionary * __null_unspecified options);
    
    // An NSItemProvider is a high level abstraction for file-like data objects supporting multiple representations and preview images.
    NS_CLASS_AVAILABLE(10_10, 8_0)
    @interface NSItemProvider : NSObject <NSCopying>
    
    // Initialize an NSItemProvider with a single handler for the given item.
    - (instancetype)initWithItem:(nullable id <NSSecureCoding>)item typeIdentifier:(nullable NSString *)typeIdentifier NS_DESIGNATED_INITIALIZER;
    
    // Initialize an NSItemProvider with load handlers for the given file URL, and the file content.
    - (nullable instancetype)initWithContentsOfURL:(null_unspecified NSURL *)fileURL;
    
    // Sets a load handler block for a specific type identifier. Handlers are invoked on demand through loadItemForTypeIdentifier:options:completionHandler:. To complete loading, the implementation has to call the given completionHandler. Both expectedValueClass and options parameters are derived from the completionHandler block.
    - (void)registerItemForTypeIdentifier:(NSString *)typeIdentifier loadHandler:(NSItemProviderLoadHandler)loadHandler;
    
    // Returns the list of registered type identifiers
    @property(copy, readonly, NS_NONATOMIC_IOSONLY) NSArray *registeredTypeIdentifiers;
    
    // Returns YES if the item provider has at least one item that conforms to the supplied type identifier.
    - (BOOL)hasItemConformingToTypeIdentifier:(NSString *)typeIdentifier;
    
    // Loads the best matching item for a type identifier. The client's expected value class is automatically derived from the blocks item parameter. Returns an error if the returned item class does not match the expected value class. Item providers will perform simple type coercions (eg. NSURL to NSData, NSURL to NSFileWrapper, NSData to UIImage).
    - (void)loadItemForTypeIdentifier:(NSString *)typeIdentifier options:(nullable NSDictionary *)options completionHandler:(nullable NSItemProviderCompletionHandler)completionHandler;
    
    @end
    
    // Common keys for the item provider options dictionary.
    FOUNDATION_EXTERN NSString * __null_unspecified const NSItemProviderPreferredImageSizeKey NS_AVAILABLE(10_10, 8_0); // NSValue of CGSize or NSSize, specifies image size in pixels.
    
    @interface NSItemProvider(NSPreviewSupport)
    
    // Sets a custom preview image handler block for this item provider. The returned item should preferably be NSData or a file NSURL.
    @property(nullable, copy, NS_NONATOMIC_IOSONLY) NSItemProviderLoadHandler previewImageHandler NS_AVAILABLE(10_10, 8_0);
    
    // Loads the preview image for this item by either calling the supplied preview block or falling back to a QuickLook-based handler. This method, like loadItemForTypeIdentifier:options:completionHandler:, supports implicit type coercion for the item parameter of the completion block. Allowed value classes are: NSData, NSURL, UIImage/NSImage.
    - (void)loadPreviewImageWithOptions:(null_unspecified NSDictionary *)options completionHandler:(null_unspecified NSItemProviderCompletionHandler)completionHandler NS_AVAILABLE(10_10, 8_0);
    
    @end
    
    // Keys used in property list items received from or sent to JavaScript code
    
    // If JavaScript code passes an object to its completionFunction, it will be placed into an item of type kUTTypePropertyList, containing an NSDictionary, under this key.
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionJavaScriptPreprocessingResultsKey NS_AVAILABLE(10_10, 8_0);
    
    // Arguments to be passed to a JavaScript finalize method should be placed in an item of type kUTTypePropertyList, containing an NSDictionary, under this key.
    FOUNDATION_EXTERN NSString * __null_unspecified const NSExtensionJavaScriptFinalizeArgumentKey NS_AVAILABLE_IOS(8_0);
    
    // Errors
    
    // Constant used by NSError to distinguish errors belonging to the NSItemProvider domain
    FOUNDATION_EXTERN NSString * __null_unspecified const NSItemProviderErrorDomain NS_AVAILABLE(10_10, 8_0);
    
    // NSItemProvider-related error codes
    typedef NS_ENUM(NSInteger, NSItemProviderErrorCode) {
        NSItemProviderUnknownError                                      = -1,
        NSItemProviderItemUnavailableError                              = -1000,
        NSItemProviderUnexpectedValueClassError                         = -1100,
        NSItemProviderUnavailableCoercionError NS_AVAILABLE(10_11, 9_0) = -1200
    } NS_ENUM_AVAILABLE(10_10, 8_0);
    

    NSItemProvider结构说明

    方法 说明
    initWithItem:typeIdentifier: 初始化方法,item为附件的数据,typeIdentifier是附件对应的类型标识,对应UTI的描述。
    initWithContentsOfURL: 根据制定的文件路径来初始化。
    registerItemForTypeIdentifier:loadHandler: 为一种资源类型自定义加载过程。这个方法主要针对自定义资源使用,例如自己定义的类或者文件格式等。当调用loadItemForTypeIdentifier:options:completionHandler:方法时就会触发定义的加载过程。
    hasItemConformingToTypeIdentifier: 用于判断是否有typeIdentifier(UTI)所指定的资源存在。存在则返回YES,否则返回NO。<br />该方法结合loadItemForTypeIdentifier:options:completionHandler:使用。
    loadItemForTypeIdentifier:options:completionHandler: 加载typeIdentifier指定的资源。加载是一个异步过程,加载完成后会触发completionHandler。
    loadPreviewImageWithOptions:completionHandler: 加载资源的预览图片。

    由此可见,其结构如下图所示:

    层次结构图

    为了要取到宿主程序提供的数组,那么只要关注loadItemTypeIdentifier:options:completionHandler方法的使用即可。有了上面的了解,那么接下来就是对inputItems进行数据分析并提取了,这里以一个链接的分享为例,改写视图控制器中的didSelectPost方法。看下面的代码:

    - (void)didSelectPost
    {
        __block BOOL hasExistsUrl = NO;
        [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem * _Nonnull extItem, NSUInteger idx, BOOL * _Nonnull stop) {
    
            [item.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]])
                                              {
                                                  NSLog(@"分享的URL = %@", item);
                                              }
    
                                          }];
    
                    hasExistsUrl = YES;
                    *stop = YES;
                }
    
            }];
    
            if (hasExistsUrl)
            {
                *stop = YES;
            }
    
        }];
    
        // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
        // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
    //    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
    }
    

    上面的例子中遍历了extensionContext的inputItems数组中所有NSExtensionItem对象,然后从这些对象中遍历attachments数组中的所有NSItemProvider对象。匹配第一个包含public.url标识的附件(具体要匹配什么资源,数量是多少皆有自己的业务所决定)。** 注意:在上面代码中注释了[self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];这行代码,主要是使到视图控制器不被关闭,等到实现相应的处理后再进行调用该方法,对分享视图进行关闭。** 在下面的章节会说明这一点。

    2.3.2 将分享数据传递给容器程序

    上面章节已经讲述了如何取得宿主应用所分享的内容。那么,接下来就是将这些内容传递给容器程序进行相应的操作(如:在一款社交应用中,可能会为取得的分享内容发布一条用户动态)。在默认情况下,iOS的应用是存在一个沙盒里面的,不允许应用与应用直接进行数据的交互。为此,苹果提供了一项叫App Groups的服务,该服务允许开发者可以在自己的应用之间通过NSUserDefaults、NSFileManager或者CoreData来进行相互的数据传输。下面介绍如何激活App Groups服务:

    • 首先要有一个独立的AppID(带通配符*号的AppID是不允许激活App Groups的)
    使用AppGroup
    • 然后打开容器应用的项目配置的Capabilities页签,激活App Groups特性,如图:
    激活AppGroup特性
    • 点击+号添加一个App Groups,点击OK按钮
    设置Group名称
    • 创建完成后,XCode会自动把应用添加到新建的分组中。如图:
    容器程序启用AppGroup
    • 上述步骤完成后,容器程序的App Groups已经算是设置完成。然后轮到Share Extension插件需要激活App Groups服务,设置步骤跟容器程序相同,唯一不同的是,插件不需要创建新的App Group,只要加入到容器程序刚才创建的Group即可(这里可以理解为,哪些应用要实现共享数据,那么他们必须在同一个Group里面)。如图:
    扩展程序启用AppGroup

    至此,应用和扩展的App Groups服务都已经启动,现在就要进行分享内容的传输操作。下面分别介绍一下NSUserDefaults、NSFileManager以及CoreData三种方式是如何实现App Groups下的数据操作:

    • NSUserDefaults:要想设置或访问Group的数据,不能在使用standardUserDefaults方法来获取一个NSUserDefaults对象了。应该使用initWithSuiteName:方法来初始化一个NSUserDefaults对象,其中的SuiteName就是创建的Group的名字,然后利用这个对象来实现,跨应用的数据读写,代码如下:
    //初始化一个供App Groups使用的NSUserDefaults对象
    NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];
    
    //写入数据
    [userDefaults setValue:@"value" forKey:@"key"];
    
    //读取数据
    NSLog(@"%@", [userDefaults valueForKey:@"key"]);
    
    • NSFileManager:通过调用 containerURLForSecurityApplicationGroupIdentifier:方法可以获得AppGroup的共享目录,然后在此目录的基础上实现任意的文件操作。代码如下:
    //获取分组的共享目录
    NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cn.vimfung.ShareExtensionDemo"];
    NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"demo.txt"];
    
    //写入文件
    [@"abc" writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];
    
    //读取文件
    NSString *str = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"str = %@", str);
    
    • CoreData:其实CoreData是基于NSFileManager取得共享目录后来实现数据共享的。即在初始化CoreData时,先使用NSFileManager取得共享目录,然后再指定共享目录为存储数据文件的目录(如存储的sqlite文件)。代码如下:
    //获取分组的共享项目
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cn.vimfung.ShareExtensionDemo"];
    NSURL *storeURL = [containerURL URLByAppendingPathComponent:@"DataModel.sqlite"];
    
    //初始化持久化存储调度器
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"DataModel" withExtension:@"momd"];
    
    NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    
    [coordinator addPersistentStoreWithType:NSSQLiteStoreType
                              configuration:nil
                                        URL:storeURL
                                    options:nil
                                      error:nil];
    
    //创建受控对象上下文
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    
    [context performBlockAndWait:^{
        [context setPersistentStoreCoordinator:coordinator];
    }];
    

    为了方便演示,这里会使用NSUserDefault来直接把取到的url地址保存起来。代码如下所示:

    /**
     *  点击提交按钮
     */
    - (void)didSelectPost
    {
        __block BOOL hasExistsUrl = NO;
        [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem * _Nonnull extItem, NSUInteger idx, BOOL * _Nonnull stop) {
    
            [item.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]])
                                              {
                                                  NSLog(@"分享的URL = %@", item);
                                                  NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];
                                                  [userDefaults setValue: ((NSURL *)item).absoluteString forKey:@"share-url"];
                                                   //用于标记是新的分享
                                                  [userDefaults setBool:YES forKey:@"has-new-share"];
                                              }
    
                                          }];
    
                    hasExistsUrl = YES;
                    *stop = YES;
                }
    
            }];
    
            if (hasExistsUrl)
            {
                *stop = YES;
            }
    
        }];
    
        // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
        // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
    //    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
    }
    

    2.3.3 做好分享插件的提示操作

    默认情况下,如果用户点击Post按钮后,分享界面就会消失,用户可以继续对宿主程序进行操作。这些都要靠NSExtensionContextd的completeRequestReturningItems:completionHandler:方法来实现。现在,由于在didSelectPost方法中加入了分享内容的处理,由于获取附件是一个异步过程,那么,就需要做好界面上的提示。否则,分享界面消失后由于没有操作提示,会使用户误以为界面进行卡死的状态,其实是分享内容还没有处理完成。接下来就是优化UI上的提示操作,代码如下:

    /**
     *  点击提交按钮
     */
    - (void)didSelectPost
    {
        //加载动画初始化
        UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
        activityIndicatorView.frame = CGRectMake((self.view.frame.size.width - activityIndicatorView.frame.size.width) / 2,
                                                 (self.view.frame.size.height - activityIndicatorView.frame.size.height) / 2,
                                                 activityIndicatorView.frame.size.width,
                                                 activityIndicatorView.frame.size.height);
        activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
        [self.view addSubview:activityIndicatorView];
    
        //激活加载动画
        [activityIndicatorView startAnimating];
    
        __weak ShareViewController *theController = self;
        __block BOOL hasExistsUrl = NO;
        [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem * _Nonnull extItem, NSUInteger idx, BOOL * _Nonnull stop) {
    
            [extItem.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]])
                                              {
                                                  NSLog(@"分享的URL = %@", item);
                                                  NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];
                                                  [userDefaults setValue:((NSURL *)item).absoluteString forKey:@"share-url"];
                                                   //用于标记是新的分享
                                                  [userDefaults setBool:YES forKey:@"has-new-share"];
    
                                                  [activityIndicatorView stopAnimating];
                                                  [theController.extensionContext completeRequestReturningItems:@[extItem] completionHandler:nil];
                                              }
    
                                          }];
    
                    hasExistsUrl = YES;
                    *stop = YES;
                }
    
            }];
    
            if (hasExistsUrl)
            {
                *stop = YES;
            }
    
        }];
    
        if (!hasExistsUrl)
        {
            //直接退出
            [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
        }
    }
    

    2.4 容器程序获取分享数据

    插件的工作基本上已经全部开发完成了,接下来就是容器程序获取数据并进行操作。下面是容器程序的处理代码:

    - (void)applicationDidBecomeActive:(UIApplication *)application
    {
        //获取共享的UserDefaults
        NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];
        if ([userDefaults boolForKey:@"has-new-share"])
        {
            NSLog(@"新的分享 : %@", [userDefaults valueForKey:@"share-url"]);
    
            //重置分享标识
            [userDefaults setBool:NO forKey:@"has-new-share"];
        }
    }
    

    为了方便演示,这里直接在AppDelegate中的applicationDidBecomeActive:方法中检测是否有新的分享,如果有则通过Log打印链接出来。

    至此,整个Share Extension开发的过程已经完成。

    2.5 提审AppStore的注意事项

    1. 扩展中的处理不能太长时间阻塞主线程(建议放入线程中处处理),否则可能导致苹果拒绝你的应用。
    2. 扩展不能单独提审,必须要跟容器程序一起提交AppStore进行审核。
    3. 提审的扩展和容器程序的Build Version要保持一致,否则在上传审核包的时候会提示警告,导致程序无法正常提审。

    3. 进阶研究

    3.1 对默认分享界面进行扩展

    在某些情况下,在分享界面中会加入一下其它信息的显示,或者其它的选项供用户操作。如:内容要分享给什么好友、分享内容的可见权限等等。那么,默认的分享界面( SLComposeServiceViewController)提供了相关的方法来对其进行扩展。这些方法定义如下:

    #if TARGET_OS_IPHONE
    /*
     Configuration Item Support (account pickers, privacy selection, location, etc.)
     */
    
    // Subclasses should implement this, and return an array of SLComposeSheetConfigurationItem instances, if if needs to display configuration items in the sheet. Defaults to nil.
    - (NSArray *)configurationItems;
    
    // Forces a reload of the configuration items table.
    // This is typically only necessary for subclasses that determine their configuration items in a deferred manner (for example, in -presentationAnimationDidFinish).
    // You do not need to call this after changing a configuration item property; the base class detects and reacts to that automatically.
    - (void)reloadConfigurationItems;
    
    // Presents a configuration view controller. Typically called from a configuration item's tapHandler. Only one configuration view controller is allowed at a time.
    // The pushed view controller should set preferredContentSize appropriately. SLComposeServiceViewController observes changes to that property and animates sheet size changes as necessary.
    - (void)pushConfigurationViewController:(UIViewController *)viewController;
    
    // Dismisses the current configuration view controller.
    - (void)popConfigurationViewController;
    #endif
    

    下面是方法的说明

    方法 说明
    - (NSArray *)configurationItems; 一个SLComposeSheetConfigurationItem类型的数组,默认情况下该方法返回一个nil。如果你想增加一项扩展信息,可以通过改写这个方法来增加一个SLComposeSheetConfigurationItem对象来实现。下面会介绍SLComposeSheetConfigurationItem的一些相关信息。
    - (void)reloadConfigurationItems; 重新加载配置项列表,该方法会重新触发configurationItems的调用,并且刷新配置项的变更内容。
    - (void)pushConfigurationViewController:(UIViewController *)viewController; 显示一个配置相关的视图控制器。该方法是结合我们自定义的配置项而设计,当点击某个配置项时需要更详细的选择,则可以使用此方法来现实一个视图控制器,并进行相关的配置。注:每次只允许显示一个配置视图控制器。
    - (void)popConfigurationViewController; 关闭一个配置的视图控制器。

    再来看一下SLComposeSheetConfigurationItem的声明:

    typedef void (^SLComposeSheetConfigurationItemTapHandler)(void);
    
    // Represents a user-configurable option for the compose session.
    // For allowing the user to choose which account to post from, what privacy settings to use, etc.
    SOCIAL_CLASS_AVAILABLE(NA, 8_0)
    @interface SLComposeSheetConfigurationItem : NSObject
    
    // Designated initializer
    - (instancetype)init NS_DESIGNATED_INITIALIZER;
    
    @property (nonatomic, copy) NSString *title; // The displayed name of the option.
    @property (nonatomic, copy) NSString *value; // The current value/setting of the option.
    @property (nonatomic, assign) BOOL valuePending; // Default is NO. set to YES to show a progress indicator. Can be used with a value too.
    
    // Called on the main queue when the configuration item is tapped.
    // Your block should not keep a strong reference to either the configuration item, or the SLComposeServiceViewController, otherwise you'll end up with a retain cycle.
    @property (nonatomic, copy) SLComposeSheetConfigurationItemTapHandler tapHandler;
    
    @end
    

    其属性说明如下:

    属性 说明
    title 配置项标题
    value 当前的配置值。
    valuePending YES时,显示值位置显示加载动画,NO时,显示配置的值。
    tapHandler 点击配置项的事件处理

    下面将通过使用这些方法来扩展UI,使插件增加两个配置项:一个是是否公开分享的配置项,该选项标识一个开关值。另外一个是公开权限设置项,在是否公开分享的开关为开时显示。可以选择分享给所有人还是好友。代码如下所示:

    - (NSArray *)configurationItems {
        // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
    
        //定义两个配置项,分别记录用户选择是否公开以及公开的权限,然后根据配置的值
        static BOOL isPublic = NO;
        static NSInteger act = 0;
    
        NSMutableArray *items = [NSMutableArray array];
    
        //创建是否公开配置项
        SLComposeSheetConfigurationItem *item = [[SLComposeSheetConfigurationItem alloc] init];
        item.title = @"是否公开";
        item.value = isPublic ? @"是" : @"否";
    
        __weak ShareViewController *theController = self;
        __weak SLComposeSheetConfigurationItem *theItem = item;
        item.tapHandler = ^{
    
            isPublic = !isPublic;
            theItem.value = isPublic ? @"是" : @"否";
    
    
            [theController reloadConfigurationItems];
        };
    
        [items addObject:item];
    
        if (isPublic)
        {
            //如果公开标识为YES,则创建公开权限配置项
            SLComposeSheetConfigurationItem *actItem = [[SLComposeSheetConfigurationItem alloc] init];
    
            actItem.title = @"公开权限";
    
            switch (act)
            {
                case 0:
                    actItem.value = @"所有人";
                    break;
                case 1:
                    actItem.value = @"好友";
                    break;
                default:
                    break;
            }
    
            actItem.tapHandler = ^{
    
                //设置分享权限时弹出选择界面
                ShareActViewController *actVC = [[ShareActViewController alloc] init];
                [theController pushConfigurationViewController:actVC];
    
                [actVC onSelected:^(NSIndexPath *indexPath) {
    
                    //当选择完成时退出选择界面并刷新配置项。
                    act = indexPath.row;
                    [theController popConfigurationViewController];
                    [theController reloadConfigurationItems];
    
                }];
    
            };
    
            [items addObject:actItem];
        }
    
        return items;
    }
    

    ShareActViewController实现:

    @interface ShareActViewController () <UITableViewDelegate, UITableViewDataSource>
    
    @property (nonatomic, strong) void (^selectedHandler) ();
    
    @end
    
    @implementation ShareActViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
        tableView.backgroundColor = [UIColor clearColor];
        tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        tableView.dataSource = self;
        tableView.delegate = self;
        [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
        [self.view addSubview:tableView];
    }
    
    - (void)onSelected:(void(^)(NSIndexPath *indexPath))handler
    {
        self.selectedHandler = handler;
    }
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        return 2;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
        cell.backgroundColor = [UIColor clearColor];
    
        switch (indexPath.row)
        {
            case 0:
                cell.textLabel.text = @"所有人";
                break;
            case 1:
                cell.textLabel.text = @"好友";
                break;
            default:
                break;
        }
    
        return cell;
    }
    
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
        if (self.selectedHandler)
        {
            self.selectedHandler (indexPath);
        }
    }
    

    在分享插件界面中重写了configurationItems方法,然后定义了两个配置项属性,分别是是否公开标识isPublic和公开权限act。然后创建是否公开的SLComposeSheetConfigurationItem配置项和根据isPublic的值来判断是否创建公开权限配置项。其中是否公开配置点击时会变更isPublic的值,从而达到显示或隐藏公开权限配置。而公开权限配置的点击则弹出一个选择的TableView,用于选择给定的值然后返回到分享界面。

    3.2 替换Share Extension中的默认分享界面

    如果通过扩展SLComposeServiceViewController还不能满足需求的情况下,这时候就需要自己设计一个分享视图控制器来替换默认的SLComposeServiceViewController。

    1. 首先,创建一个自定义视图控制器,如:CustomShareViewController。

    2. 然后打开扩展的Info.plist文件,删除NSExtensionMainStoryboard属性并增加一项NSExtensionPrincipalClass属性并指向CustomShareViewController(注:这里没有使用Storyboard所以要删除该属性),如图:


      Info.plist
    3. 接下来根据实际的需要来设计分享视图的展示与交互形式。

    4. 然后调用CustomShareViewController的extensionContext属性来控制扩展的提交与取消等操作(注:由于扩展中导入了关于ExtensionContext的UIViewController类目,因此,每个ViewController都带有extensionContext属性)。

    为了演示的简单性,下面的代码会通过extensionContext获取到url后,给到自定义分享视图的Label中显示,同时也提供一个提交和取消按钮,用于用户对分享内容的操作。代码如下:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
    
        //定义一个容器视图来存放分享内容和两个操作按钮
        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;
    
                            });
                        }
    
                    }];
    
                    hasGetUrl = YES;
                    *stop = YES;
                }
    
                *stop = hasGetUrl;
    
            }];
    
        }];
    }
    
    - (void)cancelBtnClickHandler:(id)sender
    {
        //取消分享
        [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"CustomShareError" code:NSUserCancelledError userInfo:nil]];
    }
    
    - (void)postBtnClickHandler:(id)sender
    {
        //执行分享内容处理
        [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
    }
    

    效果如下图所示:

    效果图

    最后附上Demo地址

    4. ShareExtension相关问题

    4.1 关于从扩展跳回自己应用的问题

    自从文章发出来后不断被问到的一个问题。其实苹果官方除了Today Extension外,其他Extension是不提供跳转接口的。所以这里总结的是一种非正常的方式。直接上代码(会不会审核被拒就得看自己人品了~,同时感谢曹宇___
    同学提供的实现代码):

    UIResponder *responder = self;
    while (responder)
    {
      if ([responder respondsToSelector:@selector(openURL:)])
      {
        [responder performSelector:@selector(openURL:) withObject:[NSURL URLWithString:@"testshare://"]];
        break;
      }
      responder = [responder nextResponder];
    }
    

    这种方式主要实现原理是通过响应链找到Host App的UIApplication对象,通过该对象调用openURL方法返回自己的应用。

    4.2 关于扩展在Safari的web图片中无法显示问题

    当我们设置NSExtensionActivationRule的NSExtensionActivationSupportsImageWithMaxCount大于0时,在相册、邮箱、信息和一些第三方App中时能够正常显示出扩展图标。但是在Safari中点击一条图片链接,然后选择分享时,图标则不能显示。可以通过改写NSExtensionActivationRule设置的方法来解决这个问题,如:

    <key>NSExtensionAttributes</key>
    <dict>
      <key>NSExtensionActivationRule</key>
      <string>SUBQUERY (
           extensionItems,
           $extensionItem,
           SUBQUERY (
           $extensionItem.attachments,
           $attachment,
           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" ||
           ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text"
           ).@count = 1
           ).@count > 0
      </string>
    </dict>
    

    将字典项改为Predicate匹配的字符串即可(感谢无所不能的StackOverflow,详细请参考:https://stackoverflow.com/questions/33311020/action-extension-to-handle-images-from-safari)。

    5. 相关文章

    iOS扩展开发攻略:Action Extension

    相关文章

      网友评论

      • Dzan:请问NSExtensionActivationRule字典中哪个属性是限制附件类型的,我想不能同时选择图片和视频,一次只能选一类,这样怎么弄?
        杰嗒嗒的阿杰:@Dzan 你看文章4.2章节,将限制条件改为predicate描述看看
        Dzan:@杰嗒嗒的阿杰 比如我设置了可以分享9张图片和一个视频,正常情况下,选择的时候我在系统照片中同时选了图片和视频,这样是允许分享到我的app(下面可分享app中会显示我的app),但是我希望的是选了两类的话不允许分享(我希望下面可分享app中不要显示我的app),只有选择一类的时候才能分享,我需要的是有属性可以自动判断或者限制只能选择一类,你可以看下微博的就是这样选两类就不出现了,选一类就出现。
        杰嗒嗒的阿杰:@Dzan 没太明白你的意思,因为扩展是被动接收分享数据的,也就是说如果宿主程序中就是设置了一个图片和视频,那你要显示还是不显示呢?
      • Dzan:比如我设置了图片和视频两种,请问怎么设置选择的时候只能选一种
      • 阿拉斯加的狗:现在唤不起 自己的App了 利用这个方法 老哥 你的可以吗
        杰嗒嗒的阿杰:@阿拉斯加的狗 你的什么系统版本?
      • jinlei_123:请问通讯录共享联系人是如何做的,为什么可以直接带手机号码跳转到指定app不需要弹出输入的界面呢
        杰嗒嗒的阿杰:这个其实跟app的扩展设计相关,如果扩展实现的是直接跳转那就不需要输入界面,如果带界面的扩展那就有输入界面,我试了几个app都有弹出界面的。其实分享扩展是不允许直接跳转的,但是你可以通过文章中说的跳转做法来试试
      • 正确的道路上用笨方法:楼主你好 如果自定义的话话 不引用SLComposeServiceViewController 的话,没办法跳转到ShareActViewController界面的,我们现在需求是需要在自定义的vc中跳转到另外一个界面进行选择设置的
      • 我的珊妮:默认分享界面如何不让其弹出键盘?大神求解
      • 我的珊妮:使用默认控制器,如何让textView不能编辑?设置了editable会导致分享界面都不会出来,使用代理方法textViewShouldBeginEditing:也会导致分享界面出不来,线程卡死!求指点?
      • 我的珊妮:我的App需要登录后才可以分享,如何提示用户需要登录后方可分享?我看微信等都有提示,系统有API吗?
        杰嗒嗒的阿杰:那你可以在扩展里面查询当前登录是否有效。又或者你们要求不严格就直接判断登录时间来看是否过时。
        我的珊妮:@杰嗒嗒的阿杰 可是app在登录后放入后台10分钟后会被杀死(此时是未登录状态啊),而我的共享中的状态却是登录的!这个怎么解?
        杰嗒嗒的阿杰:@我的珊妮 把你app的登录状态信息放到app group中与扩展进行共享,然后扩展里面实现登录判断。
      • Adgerc:大神 share Extension 怎么跳转到 自己的 aPP 像QQ 那样 从系统相册 跳转到QQ
        xu旭:@Adgerc 你是怎么解决的?
        Adgerc:@杰嗒嗒的阿杰 解决了...要在xcode 里面设置一个东西.
        杰嗒嗒的阿杰:@Adgerc 评论里面有解决方案,你去看看。
      • 金银岛:奇怪,为什么在CustomShareViewController(自定义的分享视图控制器)中,导入 pod 文件夹中的头文件都不行呢?我想要在CustomShareViewController中使用 Masonry 来进行视图的布局貌似都不行呢!楼主有遇到过这方面的问题吗?
        木子李55:podfile文件里写上两个target就可以了
        杰嗒嗒的阿杰:@金银岛 我刚才试了一下没有问题,找不到头文件,估计是你的Podfile写的target名字不是Extension的Target,要注意看podfile中写的是否对应你Extension的Target名字
      • 颓阿废:hello,我又来了。你知道怎么同时运行container App和share Extension吗,就是调试share Extension的时候,openURl之后进入容器app之后依然可以继续调试容器app。就是说一次调试,我在share Extension的断点和container App的断点都好使,能这样吗?
        颓阿废:@杰嗒嗒的阿杰 试了试。同时跑起来不可行,只要跑起来一个,另一个就停止了。我用log调试一下吧,谢谢
        杰嗒嗒的阿杰:@颓阿废 你尝试同时跑起两个target看行不?如果不行那就一个一个调也可以。不一定非得两个一起调吧?只要保证两部分操作正常就好。
        颓阿废:不知道说明白了没有,简单点就是我通过openUrl方法打开container App之后,在container App中停留了一下就闪退了,不知道哪里闪退,打断点不管用,因为container App是重新打开的,相当于没打断点,这个怎么解决你知道吗??
      • 乱了夏末丶蓝了海:我点击target之后,它没有创建scheme 这一项,然后运行,没有找到自己的app,更多里面也没有
        杰嗒嗒的阿杰:@乱了夏末丶蓝了海 在xcode顶栏就是选运行设备和模拟器那里,点击左边target会有一个下拉列表,里面就有Managed Schemes
        乱了夏末丶蓝了海:@杰嗒嗒的阿杰 Managed Schemes中加入扩展的Scheme,Managed Schemes在哪个位置呢
        杰嗒嗒的阿杰:检查你的容器项目中的Embedded Binaries是否有添加你的扩展,如果没有添加上。然后要测试扩展可以在Managed Schemes中加入扩展的Scheme就好
      • 乱了夏末丶蓝了海:他没有提示我创建scheme,然后我打开活动也没显示自己的app
        7125c29267fb:十分需要,很详细,感谢🙏
      • 颓阿废:你好大神,你前面的评论里写到shareExtension和容器app共享代码的事,比如我想在shareExtension的自定义的ShareViewController中使用自己的容器app的pods里的库,这个应该怎么勾选啊?我选中库文件之后xcode右侧面板里的target membership的对勾是不可选的状态啊
        另外我的容器app中一些常用的库的.h文件都是放在.pch文件中的,我需要在shareExtension的Build Setting中配置新的pch文件吗??还是把容器app中的pch文件重新配置到shareExtension中
        颓阿废:查了一下,找到了MobileCoreServices这个系统框架,里面有分好的类型枚举,可以用这个
        颓阿废:@杰嗒嗒的阿杰 嗯嗯,谢谢。弄好了。不过有个问题还想请假你一下,我在用NSItemProvider的hasItemConformingToTypeIdentifier方法判断share的文件类型的时候,发现有好多种类啊,比如我分享相册的图片,分别有public.jpeg,com.compuserve.gif等类型,分享视频,又有public.mpeg-4和com.apple.quicktime-movie等类型,我想接受这些文件的话都要挨个把这些类型判断一下吗?再说有可能有的type字符串我不知道啊
        杰嗒嗒的阿杰:@颓阿废 pods是可以指定那个target使用那那些库的,不需要为pods做xcode的配置
      • Sheldon_WDG:楼主你好,用你的方法,在模拟器中的文件中可以分享doc到我的app而且获取到doc,但是用真机测试的时候,可以获取到路径,但是获取不到doc文件
        Sheldon_WDG:这个是真机测试获取的路径
        /private/var/mobile/Library/Mobile Documents/com~apple~CloudDocs/school_score.xlsx

        这个是模拟器测试获取的路径
        /Users/Documents/Library/Developer/CoreSimulator/Devices/2A16B372-A167-4638-AC10-435DEB1F6B55/data/Library/Mobile Documents/com~apple~CloudDocs/school_score.xlsx

        模拟器的可以获取文件,但是真机不行
        Sheldon_WDG:@杰嗒嗒的阿杰 没有错误提示
        杰嗒嗒的阿杰:@Sheldon_WDG 提示什么错误呢?
      • ab25dabce3e8:如何获取标题跟图片呀,这两个一直获取不到
      • 只爱随便看看:请问在照片库中分享 类似QQ的那样,点击 发送给好友 直接跳到qq里面 如何实现
        杰嗒嗒的阿杰:@只爱随便看看 appgroup就是共享数据而已。当然你也可以在插件里面请求你的服务器。
        只爱随便看看:@杰嗒嗒的阿杰 我做的一个功能,类似把图片保存一个百度云盘的某个位置效果,数据不是一个联系人列表那样,而是一层一层的路径,那这些路径数据都需要共享在appgroup中吗,有其他方法吗
        杰嗒嗒的阿杰:这个用openurl方法跳回自己应用就好了。因为插件不能直接open,你查一下文章下的评论,有同学已经说了实现方式:blush:
      • MTDeveloper:大神 ,再问你个问题,比如:我在百度页面“分享网页”到自己app的时候,如何截取到百度页面的图,网上说的方法我都试了,不行!!!麻烦大神赐教
        杰嗒嗒的阿杰:@iOSDeveloper110 url拿到手后可以通过创建一个webview加载url,等加载完毕后进行webview的截图。
      • MTDeveloper:你好,我如果从百度分享网页到自己的app的时候,遍历不到图片,只能提取到url和title,请问有什么方法能够取到没有图片的网页分享呢?
        杰嗒嗒的阿杰:@zuoziwen 得到url后再通过url获取网页内容的标题和图片
        ab25dabce3e8:这问题你解决了吗.,我只拿到了url ,title跟图片都拿不到
        杰嗒嗒的阿杰:@iOSDeveloper110 可以根据url提取html中的图片
      • Alan_yo:想问下分享系统的照片该怎么通过路径取出这张图片?
        杰嗒嗒的阿杰:还是加载附件的形式去实现。
      • 3767d1e0a5fb:真机有错误
        Error Domain=NSCocoaErrorDomain Code=4097
        请问怎么解决
        杰嗒嗒的阿杰:@白小先生 :blush:
        3767d1e0a5fb:@杰嗒嗒的阿杰 我的错,调试忘记信任应用了
        杰嗒嗒的阿杰:@白小先生 有更详细的错误描述吗?
      • 畅游驿站:大神,我可以用Share Extension分享QQ里的PDF文件到我的软件里么?请问要怎么实现呢?
        杰嗒嗒的阿杰:@梦相随Gg 文件类型就系附件的类型属性,你从Safari过来的,那么他就是public.url。
        Glang0:@杰嗒嗒的阿杰 你好,这两天在研究这个,然后看到你这个文章,实现了利用Safari分享,然后app拿到了url地址,想请教下,如何拿到文件类型的,可以说的详细点吗? 非常感谢:pray:
        杰嗒嗒的阿杰:@畅游驿站 先确认QQ的PDF是否是调用系统分享面板。如果是的话,那你按照文章说的创建分享扩展就可以了,只是处理附件的时候看一下pdf对应是什么类型的type,然后做相应的处理。
      • Alan_yo:在 share Extensions 自定义一个 VC可以加载网络图吗 ? 还是要拿宿主 App 存储的图片?
        杰嗒嗒的阿杰:@Alan_yo 客气:grin:
        Alan_yo:@杰嗒嗒的阿杰 好的,谢谢大神指点
        杰嗒嗒的阿杰:@Alan_yo 可以加载网络图,根据自己的需要去实现就好。
      • MTDeveloper:你好,如果我是自己的app分享的话,怎么样才能在分享面板里屏蔽掉自己的app
        杰嗒嗒的阿杰:@iOSDeveloper110 :smile:网上很多
        MTDeveloper:@杰嗒嗒的阿杰 先出来效果再说吧:smile: ,这私有方法你在哪找到的?
        杰嗒嗒的阿杰:@iOSDeveloper110 只能调整UIActivityViewController的私有方法,不过通不通过审核,我无法保证。具体代码如下:
        @interface UIActivityViewController (Private)

        - (BOOL)_shouldExcludeActivityType:(UIActivity*)activity;

        @EnD

        @Implementation UIActivityViewController (Private)

        - (BOOL)_shouldExcludeActivityType:(UIActivity *)activity
        {
        if ([activity.activityType isEqualToString:@"你的ShareExtension的BundleId"])
        {
        return YES;
        }

        return NO;
        }

        @EnD
      • MTDeveloper:自己的app分享内容的时候,如何隐藏自己的app呢???
      • d759d0c7ffb9:Sp p w s
      • 曹宇___:{
        UIResponder *responder = self;
        while (responder) {
        SEL openSelector = NSSelectorFromString(@"openURL:");
        if([responder respondsToSelector:openSelector]) {
        [responder performSelector:openSelector withObject:[NSURL URLWithString:@"xxx://xxxx"] afterDelay:1.0];
        break;
        }
        responder = [responder nextResponder];
        }

        4楼回复中的这种跳转回app的方式,苹果允许吗?能通过审核吗?麻烦有经验者告知,谢谢!
        阿拉斯加的狗:@曹宇_acdc 现在唤不起 自己的App了 利用这个方法 老哥 你的可以吗
        曹宇_acdc:@DDDDeveloper 过了
        DDDDeveloper:审核通过了吗?
      • MTDeveloper:有个疑问就是,像取到"file:///var/mobile/Media/DCIM/103APPLE/IMG_3097.PNG"这种路径文件为啥保存到本地沙盒的时候是空的,从相册分享的图片,word文档,也是一样!望大神指点
        MTDeveloper:@杰嗒嗒的阿杰 就是url能拿到 但是在自己工程里面把url文件路径转成NSData类型后data是空的
        MTDeveloper:@杰嗒嗒的阿杰 保存的url路径呀 就用你文章中的保存方式
        杰嗒嗒的阿杰:@iOSDeveloper110 你是怎么保存呢
      • MTDeveloper:你好,分享网页的话如何获取网页的缩略图,就像微信分享那样,能够取到网页的图片,而不是现在的只取到一个url地址???
        杰嗒嗒的阿杰:@iOSDeveloper110 检查input中是否包含网页截图。如果没有自己可以尝试创建一个webview把网址加载进去,然后截图。:relieved:
      • MoussyL:大神,又来叨扰了:joy:
        文中运行share target时候,需要选择一个宿主应用,比如Safar,我想知道的是,当把这个app打包后,装到测试的手机上时,是否能再其他系统应用,比如Safari,或者系统相册等里打开分享找到自己的app ?
        谢啦!!☆⌒(*^-゜)v
        MoussyL:@杰嗒嗒的阿杰 OK
        杰嗒嗒的阿杰:@木子夕 可以的
      • MoussyL:楼主写的太好了,非常详细,帮了很大的忙,谢谢楼主:pray:
        有些问题想咨询下,我在Safari里点开一个网页,点击分享里的系统邮件,然后是present出来的一个页面,是个发邮件的页面,分享的链接也会默认带进去,我想问的就是,这个页面是可以自己定制的,但是present的效果怎么去实现,我的CustomShareViewController是继承于UIViewController的,谢谢楼主啦!
        杰嗒嗒的阿杰:@木子夕 :blush::blush:
        MoussyL:@杰嗒嗒的阿杰 又长知识了,我都没想到这种思路,楼主果然是大神~~~~:+1: :+1: :+1: 谢谢啦~
        杰嗒嗒的阿杰:@木子夕 CustomShareViewController不做present,但是可以当作透明容器,然后再从CustomShareViewController中present出来一个其他VC,这个VC就是实际的操作UI,这样就有present的效果了
      • 大王的锅:插件的图标怎么显示自己app的图标?
        杰嗒嗒的阿杰:@大王的锅 额,我就没有设置自动根据应用图标变化的:flushed:
        大王的锅:@杰嗒嗒的阿杰 没有,我的扩展图标的位置是空白的,在plist里面单独设置的,已经解决了,谢谢
        杰嗒嗒的阿杰:扩展的图标跟应用是一致的,不需要单独设置
      • wsxiaoluob:大神你好,请教一个功能:直接打开微信的share extension,就是在Safari的share中微信会打开一个自定义的“发送给朋友”,“分享到朋友圈”,“收藏”三个选项的页面,我想不弹出UIActivityViewController的情况下直接打开这个微信的share extension,请问下能做到么?
      • rectinajh:请问如何设置share extension的图片,默认都是没有图片的?
        杰嗒嗒的阿杰:@rectinajh 呃,你的应用的图标就是extension的图标。
        rectinajh:不是这个意思,添加share extension的icon,如何添加呢?
        杰嗒嗒的阿杰:@rectinajh 有没有图片是跟用户分享的内容和宿主应用的逻辑有关,share extension无法去控制这些。当你可以在扩展里面针对没有图片的情况作处理,例如加默认图,或者让用户选择配图
      • e69126bf50ba:大神,我问下我点了post后怎么打开我的app
        CoderXLL: @杰嗒嗒的阿杰 设置url scheme还是可以打开自己的APP的,微信,QQ在share搜索里有做
        杰嗒嗒的阿杰:@e69126bf50ba 没办法打开app的,但是与app交换数据是可以的,详细的说明看我的文章里面写的
        e69126bf50ba:我在研究了会,发现原来不需要打开app
      • 天亮説晚安:大神,又有问题了。分享浏览器内容到微信,如果微信没有登录,浏览器会蹦出页面提示你没有登录微信。我现在想做这个功能,点击safrai页面分享按钮后提示我现在做的app没有登录。第一个问题:请问点击safrai页面分享按钮之后,有什么事件可以做一些判断吗?
        第二个问题:NSExtensionPrincipalClass 现在对应的是我app正常登陆后的分享的viewController页面,那我如何让我显示的第一个页面是提示没有登陆的页面,也要绑定在NSExtensionPrincipalClass 上吗?
        我的珊妮:你是如何实现第一个问题的啊?遇到同样的问题?求解?
        杰嗒嗒的阿杰:@天亮説晚安 第一,是否登录请根据本地缓存或者请求服务器的接口来实现。第二,PrincipalClass是扩展的入口类型,入口类型只有一个不能指定多个,要显示不同的VC,可以通过present或者push,甚至addChildViewCotronller来实现,具体看应用需求。
      • 大王的锅:从shareextension 可以直接访问程序的数据库吗?
        杰嗒嗒的阿杰:@大王的锅 扩展和容器App是不同沙箱,无法路径相同
        大王的锅:@杰嗒嗒的阿杰 路径相同,数据库创建时有相同的名字的话直接使用就可以了,这样还需要app Group 吗?
        杰嗒嗒的阿杰:@大王的锅 可以,用appGroup特性共享数据库文件
      • 蜗牛1992:Extension配置每次下面的两个都会报错Add the app groups feature to your app id.Add app groups to your App Id.请问配置哪里出错的呀?
        杰嗒嗒的阿杰:@蜗牛1992 :smile:
        蜗牛1992:@杰嗒嗒的阿杰 我开了,后来发现是因为我的Bunlde Indetifier 的问题,换了一个就好了.
        杰嗒嗒的阿杰:@蜗牛1992 你的appgroup特性要在苹果开发者后台勾选才会启用
      • 天亮説晚安:大神,请问能否在替换Share Extension中默认分享界面后的CustomShareViewController中设置配置项呢?
        杰嗒嗒的阿杰:@天亮説晚安 你的CustomShareViewController继承的是SLComposeServiceViewController就可以,否则就不可以。
        天亮説晚安:@杰嗒嗒的阿杰 指是的SLComposeSheetConfigurationItem,在自定义的CustomShareViewController中增加SLComposeSheetConfigurationItem,可以吗?
        杰嗒嗒的阿杰:@天亮説晚安 你说的配置指哪些?
      • 天亮説晚安:大神,请问在扩展中能获取到Containing App内的图片资源和类文件吗?
        杰嗒嗒的阿杰:@木子夕 不是说资源无法共享,是说要把资源放到app group里面才可以。
        MoussyL:@杰嗒嗒的阿杰 楼主,我看了下QQ的分享扩展,在Safari里随便一个链接分享到QQ,弹出的分享框下边一般有3个选项 发送给好友,分享到QQ空间,我的收藏,点击发送给好友是可以正确展示出来好友列表的,这个和你说的资源无法共享是不是有矛盾? 还是我理解错了? 万分感谢~:pray:
        杰嗒嗒的阿杰:@天亮説晚安 不能的,他们是独立进程,这些资源无法共享
      • 杨丨木槿:系统相册里分享视频会有优酷土豆的选项,但是我的app里使用UIActivityViewController里面没有优酷,这是为什么?能否添加进去呢?
      • 大王的锅:只能在ios10适用吗?还是说8以上都行
        杰嗒嗒的阿杰:@大王的锅 fmdb只是第三方操作sqlite的库而已,要能访问做好app group权限就可以了
        大王的锅:@杰嗒嗒的阿杰 如果想访问主程序的数据库,需要添加fmdb吗
        杰嗒嗒的阿杰:@大王的锅 8以上都可以
      • 小番茄阳阳:你好,请问怎样才可以把APP放在下面,可以选择
        杰嗒嗒的阿杰:@iOS_贵 在UIActivityViewController中直接增加一项Activity,这个不属于分享扩展内容。
        小番茄阳阳:@杰嗒嗒的阿杰 对,我要做相册的分享
        杰嗒嗒的阿杰:你是说分享面板的最底下一栏?
      • Echo126:你好。1.怎么设置默认的ShareViewController的高度和cancel和post为 “取消”,“发送”我手机设置的是中文,但是显示出来的分享界面英文?2.ShareViewController中间的显示框有点透明,我想设置它的背景颜色,不想显示半透效果。
        杰嗒嗒的阿杰:@Echo126 backgroundColor不是可以设置么?如果不行那就自定义一个,想怎么改都行:smile:
        Echo126:@杰嗒嗒的阿杰 谢谢了。我的第二个问题意思是包含取消发送,右边图片,左边textview,下边设置的item的整个view,我不知道怎么设置它的背景色?
        杰嗒嗒的阿杰:@Echo126 系统的barbuttonitem是可以根据语言自动切换显示文字的。你的是英文说明你的app只支持英文语言,尝试添加或这更改默认语言。导航栏颜色可以通过bartintcolor来设置,自己搜搜网上有很多。
      • Hany鸿辉:你好,请教下。容器app能通过分享扩展获取到数据吗?我的app附带了一个分享扩展,在didSelectedPost方法里面nslog,发现其他safari点击post能触发nslog,但容器app点击post却没有进入方法...请问这是正常吗?谢谢
        Hany鸿辉:@杰嗒嗒的阿杰 我举个例子,我同时开发两个东西,一个是容器程序“A”,一个是分享扩展“a”。safari可以触发到a的方法,但A无法触发,我想做到A也能触发a,不知道可不可以
        Hany鸿辉:@杰嗒嗒的阿杰 我运行的是容器程序
        杰嗒嗒的阿杰:@Hany鸿辉 你要确认你当前的调试程序是容器程序还是分享扩展程序。行为应该是一致才对
      • zero000:今天用iOS10测试了下,在didSelectPost方法中去取出url并保存,取不到,这是为什么呢?
        zero000:@zero000 另外,我想问下:completeRequestReturningItems: completionHandler:对于这个方法我们可以在completionHandler中做些什么操作呢?
        zero000:@杰嗒嗒的阿杰 恩 在demo中你使用的是自定义页面的分享,我使用的是系统的分享页面,用系统的分享页面会出现这个问题
        杰嗒嗒的阿杰:@zero000 你是怎么保存呢?我直接用demo测试没有发现问题
      • zero000:您好 我想问下,这个分享是只有在Safari中可以用吗,在其它的浏览器中呢,比如谷歌,火狐之类的,另外在第三方应用,例如:qq,微信,微博中可以用吗?
        杰嗒嗒的阿杰:@zero000 公开权限是你自己的应用的功能来的,这需要你的扩展从你的应用里面去读取这些信息,然后显示在界面上,最后在提交的时候把这些信息提交给你的应用进行处理。这不属于iOS系统的功能。
        zero000:@杰嗒嗒的阿杰 嗯,好的 ,谢谢,还有一个问题,我看代码中公开权限设置这一块,配置属性中并没有真正关于权限限制的,都是些UI显示处理,那么真正要限制权限是不是要我们到宿主应用中额外去处理?
        杰嗒嗒的阿杰:@zero000 这个扩展是iOS提供的,所以只要你提及到的这些应用有调用UIActivityViewController这个来实现分享那么就会出现你的扩展。如果你不确定有没有调用内置分享菜单,那么看这个应用的分享菜单样式就能够判别了:smile:
      • CholMay:顶 :smile:
      • sunney0:恩恩,已经下载,学习下,谢谢你 :grin: ,一起进步
        杰嗒嗒的阿杰:@sunney0 我还没尝试过从扩展跳转回去容器程序,不知道可不可行。如你所说的,如果不行有没有打印什么日志信息,因为iOS9.0之后是需要加白名单的,不知道你加了没呢?
        其实扩展是另一种与应用交互的途径,如果还要结合url scheme来进行传输,感觉有点违背苹果之前设计的初衷。
        sunney0:@杰嗒嗒的阿杰 你好,请教个问题,不知你做了么:

        如你的例子,我取到了safria打开的网页的网址后,想点击post将网址付给Containing App界面的一个lable中,,我要怎么跳转呢?
        查阅资料说,给Containing App添加url schemes(如:appextension),利用下面方法调转。我把下面代码写到了post按钮的代码里,但是不能跳转。有别的方法跳转到Containing App么? :grin:
        [self.extensionContext openURL:[NSURL URLWithString:@"appextension://"]
        completionHandler:^(BOOL success) {
        NSLog(@"open url result:%d",success);
        }];
        杰嗒嗒的阿杰:@sunney0 :blush:共勉
      • sunney0:请问有demo么,能分享下么 :smile: ,谢谢
        杰嗒嗒的阿杰:@sunney0 已经在文章最后附上demo的下载地址
      • 风语随风:系统那个带textview的弹窗你怎么隐藏的?
        MoussyL:@巧克力色白 我也遇到同样的问题了,请问你是怎么解决的,谢谢啦
        巧克力色可可:@杰嗒嗒的阿杰 这个我设置了 然后 就是先出现 自定义的视图,系统的那个也弹出来了,你有没有这种情况
        杰嗒嗒的阿杰:@风语随风 info.plist中的NSExtensionPrincipalClass属性设置成自己写的UIViewController就可以了:blush:

      本文标题:【iOS扩展开发攻略】Share Extension

      本文链接:https://www.haomeiwen.com/subject/nwpyettx.html