面向 Extension 开发 🌞 Share Extension

封面图片

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!]
    }

这个方法返回了一个数组,就是对应的按钮等内容。每个按钮其实也很简单。只有 titlevaluetapHandlervaluePending 四个属性。

  • 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 就可以。

  1. 设置主app 的url type
  2. 跳转

所以我在 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 就好了。这个就不再详细的说了。到此,我能想到的功能,就基本上完成了。如果有更多需求也可以跟我讨论。

demo地址

原文地址: 面向 Extension 开发 🌞 Share Extension

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

推荐阅读更多精彩内容