美文网首页恩美第二个APP项目iOS-分享
面向 Extension 开发 🌞 Share Extensio

面向 Extension 开发 🌞 Share Extensio

作者: CepheusSun | 来源:发表于2017-07-27 18:30 被阅读361次
    封面图片

    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

    相关文章

      网友评论

        本文标题:面向 Extension 开发 🌞 Share Extensio

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