恩美第二个APP项目iOS-分享

面向 Extension 开发 🌞 Share Extensio

2017-07-27  本文已影响361人  CepheusSun
封面图片

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 四个属性。

在上面的代码里,我用 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)
        })
    }

代码中分别是分享网页和图片两个东西。这一步解决了找到分享的内容的代码。

具体分享的行为可以有两个办法来解决

这个过程就不再叙述了。

info.plist

既然说到了 info.plist 中的设置,就再看看这部分是说的什么吧!都是一些很固定的内容,我随便挑两个说说吧!

Key Description
NSExtensionActivationSupportsAttachmentsWithMaxCount 附件最大个数
NSExtensionActivationSupportsAttachmentsWithMinCount 附件最小个数
NSExtensionActivationSupportsFileWithMaxCount 附件种类限制
NSExtensionActivationSupportsMovieWithMaxCount 视频个数限制
NSExtensionActivationSupportsImageWithMaxCount 图片个数限制
NSExtensionActivationSupportsText 是否支持文本类型
NSExtensionActivationSupportsWebURLWithMaxCount web 链接最多限制
NSExtensionActivationSupportsWebPageWithMaxCount web 页面最多限制

如果要设置你的 extension 只支持图片,url 什么的。只需要把个数限制写成 0!

但是设置的时候需要注意是将NSExtensionActivationRule 改成 Dictionary 类型并添加:

这就基本上完成了,我们要在 系统或者 外部 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

上一篇 下一篇

猜你喜欢

热点阅读