Share Extension 使用户在使用其他的app 的时候, 更加方便的将其内容分享出去,像是社会化分享还有上传服务器。比如说, 在一个 app 中有个分享按钮, 用户可以选择其中一个 Share Extension 来发表评论或者内容。
写在前面的话
最好的 Share Extension 能够让用户能够很轻松的分享网页中的内容。如果你需要用一个扩展来让用户使用这些内容做一些其他的操作, 或者为用户提供他们所关心的内容的更新, Share Extension 可能就不是最好的解决方案了。
如何理解 Share Extension
Share Extension 有以下几个特点:
- 让用户更容易分享内容。
- 如果可以的话,能够让用户预览,编辑,标注,并且自定义内容。
- 在用户发送内容的时候,能够确保内容是合法的。
用户能够通过系统提供的 UI 来获得他能够使用的 Share Extension。在 iOS 中, 用户点击分享按钮,然后从系统弹出来的分享区域中选择一个 Share Extension。
当用户选择了你的 Share Extension 之后,你需要展示一个包含了内容的视图,然后发表出去。你可以将你的视图机遇系统提供的 vc, 或者自定义一个。系统提供的那个提供了一些很常见的操作,比如说,预览,合法性判断,同步内容,以及视图的动画,还有设置发布。
创建 Share Extension
创建的过程类似于之前写的 面向 Extension 开发 🌞 Today Extension
唯一不同的是 Today Extension 有唯一的一个 宿主 app 而 Share Extension 在使用的时候, 可能有很多的宿主 app 所以在运行的时候,需要选择一个宿主 app。 一般都是选择的 Safari 然后,随便打开一个网页,下面的分享按钮就可用了,点击之后,在分享列表里面就能够看到你的 app 咯。
需要注意的是,这个时候看到的 Share Extension 的名称是你 Share Extension 的名称,这个是可以更app 名称不一样的。只要改 Share Extension 的 info.plist 中的 Bundle display name 为你想要的名称就可以了。
这篇文章要做什么?
写到这里, 基本上已经完成了准备工作了。可能还有 创建 app groups 之类的工作,这块将在下面的内容中介绍。花了几天时间断断续续的研究 Share Extension
,对比了系统中本来就存在的facebook twitter 以及国内的微博什么的。我将在本文中模仿着做一个类似的效果出来。
这是最终效果的 gif 图。这只是第一步。好了,我们开始吧。
基本设置
override func viewDidLoad() {
super.viewDidLoad()
placeholder = "分享到微博" // 占位文字
charactersRemaining = 140 // 左下角的文字 展示数字,可以用来倒数,还能输入几个字, 小于等于0的时候变成红色
}
如注释所见,这里设置了placeholder 已经右下角的数字。
// 过滤分享的内容
override func isContentValid() -> Bool {
charactersRemaining = 140 - contentText.characters.count as NSNumber
return contentText.characters.count > 2
}
这段代码用来验证用户输入的内容是否合法。这里我只是简单的设置了内容的长度不能超过140,并且不能小于2.
系统在SLComposeServiceViewController
中提供了open func didSelectPost()
、open func didSelectCancel()
两个方法分别是上面两个按钮的事件。
需要注意的是,重写 cancel 的时候,需要调用 super
接下来是设置位置,分组这些内容。这写也是在系统的api 中能找到对应的方法。
override func configurationItems() -> [Any]! {
// 定位
let item1 = SLComposeSheetConfigurationItem()
item1?.title = "位置"
item1?.value = "无"
item1?.valuePending = false
item1?.tapHandler = {
item1?.valuePending = true
// 在这里做定位的操作
// 模拟花了3s时间
delay(3, task: {
item1?.value = ""
item1?.valuePending = false
item1?.value = "四川省 成都市"
})
}
// 跳转
let item2 = SLComposeSheetConfigurationItem()
item2?.title = "可见组"
item2?.value = ""
item2?.tapHandler = {
let list = ListController()
list.callbackClosure = {
item2?.value = $0
}
self.pushConfigurationViewController(list)
}
// 测试预览
/*
let item3 = SLComposeSheetConfigurationItem()
item3?.title = "预览"
item3?.tapHandler = {
let pre = self.loadPreviewView()// 这个方法实际上是用来获取右边的图片的
pre?.frame = self.view.bounds
self.view.addSubview(pre!)
}
*/
return [item1!, item2!]
}
这个方法返回了一个数组,就是对应的按钮等内容。每个按钮其实也很简单。只有 title
,value
, tapHandler
, valuePending
四个属性。
- title: 左边的文字
- value: 右边的文字
- tapHandler: 处理这个 item 事件的 closure
- valuePending: 左边转菊花的indicator,是一个 bool 类型的属性。
在上面的代码里,我用 self.pushConfigurationViewController(list)
这行代码push 到了另外的界面,用来让用户选择他们要把消息分享到的具体分组。这个操作是在 Facebook 的 share extension 中看见的。在实际中,我们也可以这样做其他很多的事情。
需要注意的是,推出来的 Controller 需要设置背景为clear,cell 也要设置背景为 clear 这是为了保证界面跟系统统一(模糊效果)。
然后就是要把用户选择的内容分享出去了。
通过 Share Extension 分享内容
要将内容分享出去,需要解决几个问题。
- 用户信息
- 获取分享的内容
因为 App Extension 和主 App 是两个不同的 Target, 这就需要我们在这个获取到主 app 中用户的登录信息。至少需要知道我们要把内容分享到哪个用户的数据流中吧。
这个其实也是很简单的事情。在 Today 中我们已经知道了 App Groups 这个东西。也知道了如何共享部分代码。
所以在 Share Extension 中
func fetchUserInfomation() -> String? {
let userdefault = UserDefaults.init(suiteName: "group.sunny.com")
let info = userdefault?.value(forKey: "userInformation") as? [String: String]
return info?["token"]
}
然后在主app 中
let userdefault = UserDefaults(suiteName: "group.sunny.com")
userdefault?.set(["token": "this the user token"], forKey: "userInformation")
userdefault?.synchronize()
就实现了数据之间的交换。到这儿,可能会想到另外一个问题。如果没有登录的话需要跳转到主 app 中进行登录操作。这里也没有什么问题通过 openurl 就可以。
- 设置主app 的url type
- 跳转
所以我在 viewDidload
方法中添加了以下代码
if fetchUserInfomation() == nil {
let alert = UIAlertController(title: "还没有登录", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "取消", style: .cancel) {_ in
self.cancel()
})
alert.addAction(UIAlertAction(title: "去登录", style: .default) {_ in
self.extensionContext?.open(NSURL(string: "sunny://action=login")! as URL, completionHandler: { (success) in
self.cancel()
print(success)
})
})
present(alert, animated: true, completion: nil)
}
判断登录状态,然后弹窗。取消或者去登录。如果选择去登录的话,就通过 openUrl 去打开主 app。
很完美吧!but it doesn't work!!!, 我在 stackoverflow 上找到了些资料。
苹果爸爸只允许 Today Extension 通过 extensionContext 的 openUrl 打开主app
但是这个需求总是需要实现的。其实还是有解决方法。
方法一: 在 Extension 中实现登录操作
这个确实没什么好说的。也是弹出一个 alert,然后输入用户名,密码,登录。完成所有操作。或者是其他什么方案,都可以。这个就不再详细描述了。Share Extension 来实现登录行为,然后 主 app 也能够共享等了状态。这仿佛也是解决了这种问题。
当然,强迫症笔者,还是想通过打开主 app 的方法来解决这个问题。
方法二: 另类的 openUrl
// For skip compile error.
func openURL(_ url: URL) {
return
}
func openContainerApp() {
var responder: UIResponder? = self as UIResponder
let selector = #selector(openURL(_:))
while responder != nil {
if responder!.responds(to: selector) && responder != self {
responder!.perform(selector, with: URL(string: "sunny://action=login")!)
return
}
responder = responder?.next
}
}
当然,上面的两个链接还有一些其他的方法,就不一一列举了。
解决了最开始的用户信息的问题。接下来就是要获取分享的内容这个问题了。在ShareExtension 中,相信已经看见了。需要两个东西,第一个是用户关于这个内容的评论,以及这个内容本身(url、照片等)。关于用户对内容的评论这点其实很简单。
用户评论
// Convenience. This returns the current text from the textView.
open var contentText: String! { get }
系统提供的这个 api 就能够解决这个问题。
附件内容
暂且叫做附件内容吧!我也不知道应该怎么叫。这个东西,我们还是看看 extensionContext
这个东西吧!
NSExtensionContext
这个类一共暴露了四个api出来。我们看第一个
// The list of input NSExtensionItems associated with the context. If the context has no input items, this array will be empty.
open var inputItems: [Any] { get }
看样子就是这个了。
看注释内容,突然感觉,apple 的api 也有设计的不是很好的地方,既然注释都明确说了 NSExtensionItems 数组应该不是 Any 的吧😂
既然这样, 我们再看看 NSExtensionItem
这个类吧!
// (optional) title for the item
@NSCopying open var attributedTitle: NSAttributedString?
// (optional) content text
@NSCopying open var attributedContentText: NSAttributedString?
// (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.
open var attachments: [Any]?
// (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.
open var userInfo: [AnyHashable : Any]?
注释太复杂了,整理成一个表格就是这样的:
Properties | Description |
---|---|
attributedTitle | 标题 optional |
attributedContentText | 内容 optional |
attachments | 所有的附件NSItemProvider 组成一个数组 optional |
userInfo | 一个key-value结构的数据。NSExtensionItem中的属性都会在这个属性中一一映射。注释中讲到的在 info.plist 中要设置的部分会在后面提到 |
下面的表格就是 userInfo
中的 key :
名称 | 说明 |
---|---|
NSExtensionItemAttributedTitleKey | 标题 的键名 |
NSExtensionItemAttributedContentTextKey | 内容 的键名 |
NSExtensionItemAttachmentsKey | 附件 的键名 |
上面又提到了 NSItemProvider
这个东西。这相必须就是我们需要的附件了吧!
Api | description |
---|---|
initWithItem:typeIdentifier: | 初始化方法,item为附件的数据,typeIdentifier是附件对应的类型标识,对应UTI的描述。 |
initWithContentsOfURL: | 根据制定的文件路径来初始化。 |
registerItemForTypeIdentifier:loadHandler: | 为一种资源类型自定义加载过程。这个方法主要针对自定义资源使用,例如自己定义的类或者文件格式等。当调用loadItemForTypeIdentifier:options:completionHandler:方法时就会触发定义的加载过程。 |
hasItemConformingToTypeIdentifier: | 用于判断是否有typeIdentifier(UTI)所指定的资源存在。存在则返回YES,否则返回NO。该方法结合loadItemForTypeIdentifier:options:completionHandler:使用。 |
loadItemForTypeIdentifier:options:completionHandler: | 加载typeIdentifier指定的资源。加载是一个异步过程,加载完成后会触发completionHandler。 |
loadPreviewImageWithOptions:completionHandler: | 加载资源的预览图片。 |
这时候看看整体的结构:(这个图是在看到的)
到这里,应该已经知道了应该怎么做了吧!
// 点击发表的事件
override func didSelectPost() {
self.extensionContext?.inputItems.forEach({ (item) in
print("//////////////////////////")
let ext = item as! NSExtensionItem
ext.attachments?.forEach({
let atta = $0 as! NSItemProvider
print(atta)
// 分享的是网页
if atta.hasItemConformingToTypeIdentifier("public.url") {
atta.loadItem(forTypeIdentifier: "public.url") { (item, error) in
print("//////////////////////////")
print(item!)
}
print("//////////////////////////")
}
// 分享的是图片
if atta.hasItemConformingToTypeIdentifier("public.jpeg") {
atta.loadItem(forTypeIdentifier: "public.jpeg") { (item, error) in
print("//////////////////////////")
print(item!)
}
print("//////////////////////////")
}
})
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
})
}
代码中分别是分享网页和图片两个东西。这一步解决了找到分享的内容的代码。
具体分享的行为可以有两个办法来解决
- 将需要分享的内容功过 apps group 保存,然后在打开主 app 的时候,在主 app 中取出然后发送给sever。
- 直接在 Share Extension 中分享。
这个过程就不再叙述了。
info.plist
既然说到了 info.plist 中的设置,就再看看这部分是说的什么吧!都是一些很固定的内容,我随便挑两个说说吧!
Key | Description |
---|---|
NSExtensionActivationSupportsAttachmentsWithMaxCount | 附件最大个数 |
NSExtensionActivationSupportsAttachmentsWithMinCount | 附件最小个数 |
NSExtensionActivationSupportsFileWithMaxCount | 附件种类限制 |
NSExtensionActivationSupportsMovieWithMaxCount | 视频个数限制 |
NSExtensionActivationSupportsImageWithMaxCount | 图片个数限制 |
NSExtensionActivationSupportsText | 是否支持文本类型 |
NSExtensionActivationSupportsWebURLWithMaxCount | web 链接最多限制 |
NSExtensionActivationSupportsWebPageWithMaxCount | web 页面最多限制 |
如果要设置你的 extension 只支持图片,url 什么的。只需要把个数限制写成 0!
但是设置的时候需要注意是将NSExtensionActivationRule 改成 Dictionary 类型并添加:
- NSExtensionActivationSupportsAttachmentsWithMaxCount
- NSExtensionActivationSupportsAttachmentsWithMinCount
- NSExtensionActivationSupportsImageWithMaxCount
- NSExtensionActivationSupportsMovieWithMaxCount
- NSExtensionActivationSupportsWebPageWithMaxCount
- NSExtensionActivationSupportsWebURLWithMaxCount
这就基本上完成了,我们要在 系统或者 外部 app 中将内容分享到我们自己的 app 中。这好像还是有很大的限制。毕竟如果我们的产品不是像微博qq这样的社交app 的话,这个东西就没什么作用了。
另外注意这个警告
在自己的app 中调起 Share Extension
let activity = UIActivityViewController(activityItems: ["百度", URL(string: "http://www.baidu.com")!], applicationActivities: nil)
// 不分享到 airDrop 和 粘贴板
activity.excludedActivityTypes = [.airDrop, .copyToPasteboard]
present(activity, animated: true, completion: nil)
当然还有 UIActivityViewControllerCompletionHandler
这个东西,来回调分享的结果。
另外一种方法可以直接调起某个系统的分享。
// 判断是否支持 微博
if !SLComposeViewController.isAvailable(forServiceType: SLServiceTypeSinaWeibo) {
// 应该是没有登录的原因, 所以一直不会返回
print("不可用")
return
}
let composeVC = SLComposeViewController(forServiceType: SLServiceTypeSinaWeibo)
// // 添加要分享的图片
// composeVC?.add(UIImage(named: "Nameless"))
// // 添加要分享的文字
// composeVC?.setInitialText("分享到XXX")
// // 添加要分享的url
// composeVC?.add(URL(string: "http://www.baidu.com"))
// // 弹出分享控制器
self.present(composeVC!, animated: true, completion: nil)
// // 监听用户点击事件
composeVC?.completionHandler = {
if $0 == .done {
NSLog("点击了发送");
} else if $0 == .cancelled {
NSLog("点击了取消");
}
}
这种方式有一个缺陷,就是,这样的分享只能对系统的分享,微信什么的就不能这么做了。
最后的话
Share Extension 写到这里就差不多了。初步的入门步骤也已经完成了。最后,我看了一下,微信的 Share Extension 做的事情,感觉用他还能做很多的事情。这个也需要在开发中根据实际需求去拓展了,另外还有自定义 UI 等,也是很简单的事情。只是用自己 UIViewController 就好了。这个就不再详细的说了。到此,我能想到的功能,就基本上完成了。如果有更多需求也可以跟我讨论。