Share Extension 开发指北
Share Extension(分享扩展)是一种iOS系统级扩展,该扩展使 iOS 应用间数据交换成为可能。
图片
除 Share Extension 外,iOS 系统级扩展还包括 Today Extension、Action Extension、Photo Editing Extension、Custom Keyboard 等,本文只讲 Share Extension 的基本用法。
言归正传
下面开始
一步步实现Share Extension。
Share Extension 不能单独创建,必须依赖于一个工程项目。
-
打开要增加扩展的项目,点击 File → New → Target
-
选择 Share Extension
-
填写 Product Name(产品名)及其他常用选项,点击Finish
-
在弹出窗口中选择“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 服务传输数据主要有 UserDefaults、FileManager、CoreData 三种方式。
使用 App Groups 服务
非常简单,只需在主程序项目配置中选择 Signing & Capabilities(Xcode 11以下是 Capabilities),添加 App Groups Capability ,然后增加一个App Groups 即可。
App Groups 跟Bundle ID 一样,只是通常以
group.
开头。
下面说一下
共享数据的三种方式
- 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")*
- 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)
- 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 基本开发套路介绍完了。
强调几个需要注意的点
- 注意内存消耗
Share Extension 只有120M的内存空间可以使用(测试环境:Xcode 11,iOS 12.3.1),所以开发时请注意内存消耗,尤其不要向UserDefaults里大量写入数据,可以采用FileManager 的 *copyItem *保存要分享的内容。
- 注意上架要求
为减少上架时不必要的麻烦,建议:
- Share Extension 的 Info.plist 中,对NSExtensionAttributes做显式声明。
以网址为例,具体做法:
* 在Info.plist将NSExtensionActivationRule字段类型由String改为Dictionary。
* 展开NSExtensionActivationRule字段,创建其子项NSExtensionActivationSupportsWebURLWithMaxCount,并设置一个限制数量。
- 确保 Share Extension 的部署目标跟主程序部署目标一致。
- 注意Share Extension 的使用限制:
- 不能访问 sharedApplication 对象
- 不能使用任何标记NS_EXTENSION_UNAVAILABLE宏的API,或者类似的宏,或者不可用framework里面的API,例如HealthKit framework不能用于App extensions
- 不能访问相机或者麦克风(iMessage app可以访问这些资源,只要在Info.plist里面进行配置使用描述即可)
- 尽量不要运行一个长时间的后台任务(根据不同平台而异)
- 不能使用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 始终无法在共享菜单里显示出来,尝试过以下几种方式:
- Share Extension 的 Info.plist 中,对NSExtensionAttributes做显式声明,发现并没用。
- Share Extension 的 Info.plist 中,增加App Transport Security Settings,设置Allow Arbitrary Loads 为 YES,也无效果;
- 使用正式版Xcode打开项目(之前用的是Beta版),无效;
- 更改 Share Extension 的部署目标跟主程序部署目标一致(iOS 10.0,iPhone),无效;
最终,通过在正式版 Xcode 中删除 Share Extension 并重新创建 Share Extension 问题得以解决,推测问题原因可能是 Beta 版 Xcode 对此存在 Bug,建议在开发时,尽量使用正式版 Xcode 进行开发,以免造成一些不必要的麻烦。
二、无法找到 kUTTypeText 等
kUTTypeText、kUTTypeImage、kUTTypeURL 在 CoreServices 内(iOS系统是 MobileCoreServices),需要
- import MobileCoreServices
- 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页面分享,需要自己设置一个数值。 |