Share Extension 开发指北

Share Extension(分享扩展)是一种iOS系统级扩展,该扩展使 iOS 应用间数据交换成为可能。


图片

除 Share Extension 外,iOS 系统级扩展还包括 Today Extension、Action Extension、Photo Editing Extension、Custom Keyboard 等,本文只讲 Share Extension 的基本用法。

言归正传

下面开始

一步步实现Share Extension。

Share Extension 不能单独创建,必须依赖于一个工程项目。

  1. 打开要增加扩展的项目,点击 File → New → Target

  2. 选择 Share Extension

  3. 填写 Product Name(产品名)及其他常用选项,点击Finish

  4. 在弹出窗口中选择“Activate”

这样,一个分享扩展就创建好了。

下面,我们可以开始编译运行这个扩展,与普通项目不一样的是:Share Extension 的运行需要选择一个Host APP(宿主应用,以下简称主程序)。这里以 Safari 为例,选中 Safari 并点击 Run,会启动 Safari ,打开任意一个网页,点击下方的

分享按钮

分享按钮,不出意外,这时候应该就可以看见你的分享扩展了。(出意外了?看下面的爬坑记录


从现在开始,我们讨论的前提就是已经创建好了 Share Extension


创建完 Share Extension 后,在项目目录中会生成一个 ShareExtension 文件夹,其中的 ShareViewController.swift 就是系统默认的入口文件。
ShareViewController 继承于 SLComposeServiceViewController,会附带一个默认的分享视图,点击分享按钮,然后点击刚创建的 Share Extension,弹出的分享弹框视图 即是。
此时如果点击 弹框 的 Post 按钮,程序会执行 didSelectPost 方法,我们只需要在此方法中实现分享逻辑即可。
这里难免会用到主程序的一些信息,比如 当前登录用户的 UserID 等。

下面,我们就来讨论一下

如何在 Share Extension 中获取主程序的信息

在默认情况下,iOS的应用是存在一个沙盒里面的,不允许应用之间直接进行数据的交互。
不过,对于开发者自己的应用,可以利用苹果提供的 App Groups 服务,在自己的应用之间进行数据传输。
一般来说,利用 App Groups 服务传输数据主要有 UserDefaultsFileManagerCoreData 三种方式。

使用 App Groups 服务

非常简单,只需在主程序项目配置中选择 Signing & Capabilities(Xcode 11以下是 Capabilities),添加 App Groups Capability ,然后增加一个App Groups 即可。

App Groups 跟Bundle ID 一样,只是通常以group.开头。

下面说一下

共享数据的三种方式

  1. UserDefaults
  • 创建

UserDefaults(suiteName: "your.app.groups.id"),注意不能用 UserDefaults.standard

  • 读取与写入

与正常的UserDefaults一致。
UserDefaults(suiteName: "your.app.groups.id")?.set("your.value", forKey: "YOURKEY")

  •    UserDefaults(suiteName: "your.app.groups.id")?.value(forKey: "YOURKEY")*
    
  1. FileManger
  • 创建

let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "your.app.groups.id")

  • 读取与写入

与正常的FileManager一致
// 写入
try FileManager.default.copyItem(at: srcURL, to: groupURL.appendingPathComponent(srcURL.lastPathComponent))
try "your.content".write(to: groupURL, atomically: true, encoding: .utf8)
// 读取
try String(contentsOf: groupURL)

  1. CoreData

其实CoreData是基于 FileManager 取得共享目录后来实现数据共享的,此处不多介绍。

通过这些方式,相信很容易可以拿到分享时需要的用户信息了,接下来,我们再来看一下

如何获取分享的内容

通过Share Extension唤醒的程序,可以通过 *self.extensionContext?.inputItems *获取到分享内容,示例代码如下:

struct MyError: Error {
    var localizedDescription: String
}

override func viewDidLoad() {
        super.viewDidLoad()

        for inputItems in self.extensionContext?.inputItems.compactMap({ $0 as? NSExtensionItem }) ?? [] {
            for itemProvider in inputItems.attachments ?? [] {
                if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
                itemProvider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil) { (data, error) in
                        
                    guard error == nil else {
                            self.extensionContext?.cancelRequest(withError: error!)
                        return
                    }
                        
                    guard let element = data as? URL else {
                            self.extensionContext?.cancelRequest(withError: MyError(localizedDescription: "获取分享内容失败"))
                        return
                    }
                        
                    // element 就是分享内容的 URL,可以在此保存备用。
                        
                }
            }
        }
    }
}

有了内容跟用户信息,接下来只需要调用网络请求即可,以Alamofire库为例,通常,我们是用CocoaPods管理第三方库的,同样也可以

用CocoaPods管理 Share Extension 的第三方库

使用方法也非常简单,只需要在Podfile里增加

target 'ShareExtension' do
    pod 'Alamofire'
end

接下来,像开发主程序一样去做就可以了。

当然,主程序也可以跟Share Extension

共享代码

只需要选中需共享的文件,在右侧功能区的target里勾选 ShareExtension 即可。
如果你不嫌复制代码low的话,直接粘贴复制当然也可实现共享代码。🤣

配置UI

ShareViewController 中的 configurationItems() 方法可以配置选择项,如分享到微信的 发送给朋友、分享到朋友圈、收藏 一样。
通常这个方法需要返回一个包含 SLComposeSheetConfigurationItem 的数组,
SLComposeSheetConfigurationItem 有 title,value, tapHandler, valuePending 四个属性,分别对应标题、值、点击事件、加载中提示四个功能。

如果需要跳转控制器,通常需要用到 *self.pushConfigurationViewController(_:) *方法。

自定义UI

如果自带的UI不能满足需求,可以完全自定义UI,只需将ShareViewController 继承于 UIViewController 即可。
值得注意的是,自定义UI时不要使用 UIScreen.main.bounds ,会拿不到数据。

至此,Share Extension 基本开发套路介绍完了。

强调几个需要注意的点

  1. 注意内存消耗

Share Extension 只有120M的内存空间可以使用(测试环境:Xcode 11,iOS 12.3.1),所以开发时请注意内存消耗,尤其不要向UserDefaults里大量写入数据,可以采用FileManager 的 *copyItem *保存要分享的内容。

  1. 注意上架要求

为减少上架时不必要的麻烦,建议:

  1. Share Extension 的 Info.plist 中,对NSExtensionAttributes做显式声明。

以网址为例,具体做法:
* 在Info.plist将NSExtensionActivationRule字段类型由String改为Dictionary。
* 展开NSExtensionActivationRule字段,创建其子项NSExtensionActivationSupportsWebURLWithMaxCount,并设置一个限制数量。

  1. 确保 Share Extension 的部署目标跟主程序部署目标一致。
  2. 注意Share Extension 的使用限制:
  3. 不能访问 sharedApplication 对象
  4. 不能使用任何标记NS_EXTENSION_UNAVAILABLE宏的API,或者类似的宏,或者不可用framework里面的API,例如HealthKit framework不能用于App extensions
  5. 不能访问相机或者麦克风(iMessage app可以访问这些资源,只要在Info.plist里面进行配置使用描述即可)
  6. 尽量不要运行一个长时间的后台任务(根据不同平台而异)
  7. 不能使用AirDrop接收数据

进阶😎

有些业务放在Share Extension 中可能会过于繁琐,会使 Share Extension 过于臃肿。于是乎,对于一些较为复杂的逻辑,或者对主程序依赖程度较高的功能,我们会考虑

在主程序中处理

例如:QQ的“发送给好友”就采用了该方案。

此方案思路较为明确:
首先,保存要分享的数据,;
然后,打开主程序;
最后,主程序获取到分享的数据并进行相关分享等操作。

保存与读取数据

我们可以用UserDefaults、FileManager、CoreData等任何一种你喜欢的方式保存或读取即可。忘了的可以回去看共享数据的三种方式

打开主程序

因为苹果爸爸是不允许 Share Extension 中使用 openURL的(Today Extension 跟 Message Extension可以),所以,这里我们需要用一点小技巧。

首先,配置URL Scheme。
在项目配置中,选择 Info,在URL Types 中输入 URL Schemes 即可。注意 URL Schemes 通常为纯英文字符,使用 _、*、&、%等特殊会导致无法打开程序(-、.可以使用)。

然后,就可以利用如下小技巧通过 URL Scheme 打开主程序
具体写法如下:

    /// 打开主APP
    /// - Parameter type: 打開類型
    func openContainerApp(type: String) {

        let scheme = "yoururlscheme://type"

        let url: URL = URL(string: scheme)!

        let context = NSExtensionContext()

        context.open(url, completionHandler: nil)

        var responder = self as UIResponder?

        let selectorOpenURL = sel_registerName("openURL:")

        while (responder != nil) {

            if responder!.responds(to: selectorOpenURL) {

                responder!.perform(selectorOpenURL, with: url)

                break

            }

            responder = responder?.next

        }

        self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)

    }

主程序打开后会调用AppDelegate 的 application(_:open:options:) 方法,可以用 url.host 获取到 ://后的type,以便进行相应的跳转或数据处理。

主程序打开了,分享的内容也有了,剩下的尽情发挥吧~

爬坑记录:

一、 Share Extension 不显示。

        第一次按步骤操作后,点击分享,Share Extension 始终无法在共享菜单里显示出来,尝试过以下几种方式:
  1. Share Extension 的 Info.plist 中,对NSExtensionAttributes做显式声明,发现并没用。
  2. Share Extension 的 Info.plist 中,增加App Transport Security Settings,设置Allow Arbitrary Loads 为 YES,也无效果;
  3. 使用正式版Xcode打开项目(之前用的是Beta版),无效;
  4. 更改 Share Extension 的部署目标跟主程序部署目标一致(iOS 10.0,iPhone),无效;

最终,通过在正式版 Xcode 中删除 Share Extension 并重新创建 Share Extension 问题得以解决,推测问题原因可能是 Beta 版 Xcode 对此存在 Bug,建议在开发时,尽量使用正式版 Xcode 进行开发,以免造成一些不必要的麻烦。

二、无法找到 kUTTypeText 等

kUTTypeText、kUTTypeImage、kUTTypeURL 在 CoreServices 内(iOS系统是 MobileCoreServices),需要

  1. import MobileCoreServices
  2. import CoreServices 后在 Linked Frameworks and Libraries中 Add CoreServices库

以上方法二选一,建议第一种。

三、无法通过 URL Schemes 启动程序

URL Schemes 中使用 _、*、&、%等特殊会导致无法打开程序,短线(-)、小数点(.)可以使用。
如果通过 URL Scheme 无法启动程序,可以在 Safari 地址栏中输入 yourURLScheme:// 试一下能否打开,如果Safari都打不开,就是URL Scheme配置问题,否则是唤起的代码存在问题。

附录

Info.plist 常用字段说明

Bundle display name 扩展的显示名称,默认跟你的项目名称相同,可以通过修改此字段来控制扩展的显示名称。
NSExtension 扩展描述字段,用于描述扩展的属性、设置等。作为一个扩展项目必须要包含此字段。
NSExtensionAttributes 扩展属性集合字段。用于描述扩展的属性。
NSExtensionActivationRule 激活扩展的规则。默认为字符串“TRUEPREDICATE”,表示在分享菜单中一直显示该扩展。可以将类型改为Dictionary类型,然后添加以下字段: NSExtensionActivationSupportsAttachmentsWithMaxCount NSExtensionActivationSupportsAttachmentsWithMinCount NSExtensionActivationSupportsImageWithMaxCount NSExtensionActivationSupportsMovieWithMaxCount NSExtensionActivationSupportsWebPageWithMaxCount NSExtensionActivationSupportsWebURLWithMaxCount
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页面分享,需要自己设置一个数值。

参考文档:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267