转载

自定义Share Extension

自定义Share Extension

作者: @nixzhu

在iOS上,若一个app要接收从其它app过来的数据,通常的做法是实现一个分享扩展。例如,从相册分享图片到你的app中,或者从Safari分享链接到你的app中。

如果你的需求比较简单,那继承 SLComposeServiceViewController 使用系统提供的UI将最方便。但如果设计上有更复杂的要求,你就只能通过自定义 UIViewController 来做了。

通常,分享扩展的起始界面由 MainInterface.storyboard 指定,如果你不想使用Storyboard,也可以修改分享扩展中的Info.plist来指定一个 NSExtensionPrincipalClass ,它可以直接继承自UIViewController,你可以 在此 找到更详细的说明。不过,我们也可以直接修改Storyboard,增加一个View Controller并指定其为Initial View Controller,然后让这个View Controller使用我们自定义的UIViewController。

如果你的自定义UI不是全屏的,我会建议你在之前的Initial View Controller里增加一个Container View,形如:

自定义Share Extension

这样,基本的UI框架就OK了。如果你要做动画,那在第一个控制器里让Container View动画即可,后续的控制器可以利用delegate来让第一个控制器做事。

UI确定后,接下来考虑获取分享数据。假设从系统相册分享图片。

在App Extension里,UIViewController新增了一个属性 var extensionContext: NSExtensionContext? ,通过它,我们可以让Initial View Controller准备好数据。我们先给 NSExtensionContext 增加一个扩展方法:

extension NSExtensionContext {

    func circle_images(in vc: UIViewController, completion: @escaping (_ images: [ShareInfo.Image]) -> Void) {
        let extensionContext = self
        guard let extensionItems = extensionContext.inputItems as? [NSExtensionItem] else {
            return completion([])
        }
        var images: [ShareInfo.Image] = []
        let imageTypeIdentifier = kUTTypeImage as String
        let group = DispatchGroup()
        for extensionItem in extensionItems {
            for attachment in extensionItem.attachments as! [NSItemProvider] {
                if attachment.hasItemConformingToTypeIdentifier(imageTypeIdentifier) {
                    group.enter()
                    var previewImage: UIImage?
                    var fileURL: URL?
                    let loadGroup = DispatchGroup()
                    loadGroup.enter()
                    attachment.loadPreviewImage(options: [:]) { secureCoding, _ in
                        defer {
                            loadGroup.leave()
                        }
                        previewImage = secureCoding as? UIImage
                    }
                    let previewImagePreferredSize = CGSize(width: 300, height: 300)
                    loadGroup.enter()
                    attachment.loadItem(forTypeIdentifier: imageTypeIdentifier, options: nil) { secureCoding, _ in
                        defer {
                            loadGroup.leave()
                        }
                        if let url = secureCoding as? URL {
                            fileURL = url
                        } else if let image = secureCoding as? UIImage {
                            if let data = UIImageJPEGRepresentation(image, 0.9) {
                                let imageName = "/(UUID().uuidString).jpg"
                                let tempImageURL = FileManager.default.temporaryDirectory.appendingPathComponent(imageName)
                                do {
                                    try data.write(to: tempImageURL)
                                    fileURL = tempImageURL
                                    let fixedSize = image.size.circle_fixed(forPreferredSize: previewImagePreferredSize)
                                    previewImage = image.yy_imageByResize(to: fixedSize)
                                } catch {
                                    vc.circle_alert(message: "/(error)")
                                }
                            }
                        }
                    }
                    loadGroup.notify(queue: .main) { [weak self] in
                        defer {
                            group.leave()
                        }
                        guard let fileURL = fileURL else { return }
                        if previewImage == nil {
                            if let image = UIImage(contentsOfFile: fileURL.path) {
                                let fixedSize = image.size.circle_fixed(forPreferredSize: previewImagePreferredSize)
                                previewImage = image.yy_imageByResize(to: fixedSize)
                            }
                        }
                        guard let previewImage = previewImage else { return }
                        let image = ShareInfo.Image(
                            previewImage: previewImage,
                            fileURL: fileURL
                        )
                        images.append(image)
                    }
                }
            }
        }
        group.notify(queue: .main) {
            completion(images)
        }
    }
}

除开两层DispatchGroup,这个方法没有难以理解的东西。但要注意的是,loadItem回调里的secureCoding既可能是一个文件URL也可能是一个UIImage(还有其他的可能性,参考 The Struggle with Action Extensions )。而且,loadPreviewImage的回调里不一定能找到UIImage。但通常,若从系统相册分享,Preview Image一般都存在;若从其它地方分享,secureCoding一般都是UIImage或文件URL,Preview Image不一定存在(虽然,按照Apple的建议,数据提供方有责任提供Preview Image)。

其中, ShareInfo 是一个类似这样的结构:

struct ShareInfo {
    struct Image {
        let previewImage: UIImage
        let fileURL: URL
    }
    var images: [Image] = []
    //...
}

注意我们用fileURL来指定原图片,而不是直接将其数据拿到生成UIImage,这里有内存占用的考量。因为在Share Extension中,我们可以使用的内存比较有限(iPhone 7上大约70MB),如果用户选择了很多图片,而我们又全部生成UIImage,那内存很可能暴涨,我们的Share Extension进程就会被iOS强制杀掉。此外,用户选择的图片可能是GIF,你可能也需要对它进行特殊的判断,超过一定的大小可能要提示用户或者放弃分享。

对于其它数据,例如Text、Web URL或者File,你可以写出类似的扩展方法。

有了数据之后,就是具体的分享操作了。如果你的app架构合理,例如使用了Framework来封装核心功能,并能在扩展中使用这些Framework,那么你会比较轻松。不然,你要整理一些分享扩展中用到的逻辑,提取代码公用。此外,就是再次关注使用fileURL时的内存占用,你可能需要一些锁机制,一次只处理一个fileURL,让内存能被及时回收。

分享完成或者放弃分享后,正确调用extensionContext的

func completeRequest(returningItems items: [Any]?, completionHandler: ((Bool) -> Swift.Void)? = nil)

func cancelRequest(withError error: Error)

来确保分享扩展被正确释放。

如果你需要在后台发送,直接使用Background Task可能不行(参考 What we learned building the Tumblr iOS share extension )。我使用的一个hack是先调用completeRequest,但在其completionHandler里等待一个信号量。这样分享扩展并不会立即被释放,让你的上传有时间在后台完成(完成后再发送信号量)。

最后,如果你的app会作为系统分享的数据源,除了数据的质量外,你有责任准备Preview Image,请参考 NSItemProvider 相关的API。

广告时间: 「圈子」1.1版 现已上线,终于可以自由建圈了,欢迎尝试!或者加入我创建的 「可爱的Bug」 圈来分享你与Bug的故事。我相信,被说出来的Bug将无处遁形。

欢迎转载,但请一定注明出处! https://github.com/nixzhu/dev-blog

原文  https://github.com/nixzhu/dev-blog/blob/master/2018-04-18-custom-share-extension.md
正文到此结束
Loading...