阅读 472

[SwiftUI 100 天] 如何保存图片到用户的相册

译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 ➕三连?关注专栏,关注我

在我们完成这个项目的技术预研之前,还有最后一块 UIKit 的拼图需要解决:一旦我们处理了用户的图片,生成一个 UIImage,我们需要将其存入用户的相册。这里用到一个叫UIImageWriteToSavedPhotosAlbum() 的 UIKit 函数。它的常规用法很琐碎,为了用好它,我们还得去 UIKit 走一遭。尤其在这种时刻,我们才会由衷地体会到 SwiftUI 的优越性。

在编写代码之前,我们需要对 Info.plist 做出一点小改动。你看,往相册里写入照片是一项受保护的操作,因此如果用户没有显式授权给我们,我们是不能做这件事的。

iOS 会负责请求权限并检查用户的响应,我们要做的是提供一个我们为什么要写入图片的简短的文本说明 。打开 Info.plist,右键空白区域,选择 Add Row。你会看到一个包含下拉选项的列表 —— 我们往下滚动,选择 “Privacy - Photo Library Additions Usage Description”。对于右边的值,输入文本 “我们想要保存您的滤镜图片。”

完成之后,我们就可以使用 UIImageWriteToSavedPhotosAlbum() 方法来写入照片了。之前我们已经实现了 loadImage() 方法:

func loadImage() {
    guard let inputImage = inputImage else { return }
    image = Image(uiImage: inputImage)
}复制代码

我们可以对它做简单的修改,在图片载入完成时立刻就保存。把下面这行添加到方法的末尾:

UIImageWriteToSavedPhotosAlbum(inputImage, nil, nil, nil)复制代码

就这样 —— 每当你导入一张照片时,我们的应用就会将其保存到图库。当你第一次在应用中尝试时,iOS 会自动提示用户,请求写入照片所需的权限并展示我们添加到 Info.plist 文件里的说明文本。

到这里,你可能不禁想 “这也太简单了吧”。某种程度上你的想法是对的,但之所以简单的原因在于我们执行了最少的工作:我们只提供了要保存的图片作为 UIImageWriteToSavedPhotosAlbum() 的第一个参数,而其余三个参数都给的是 nil

那些 nil 参数其实很重要,至少其中的前两个做了两件事:告诉 Swift 当保存完成时要调用什么方法,同时通知我们保存操作成功与否。如果你对这些逻辑都不关心,那么像之前那样的参数就可以了 —— 三个都传入 nil。但是请注意:用户可能会拒绝你访问他们的相册,所以如果你不捕捉保存错误,用户可能会误以为你的应用没有正常工作。

之所以需要用到两个参数来获取要调用的方法的信息,是因为这个 API 是旧的 —— 远比 Swift 老,实际上,这个 API 出现在 Objective-C 里等价闭包的语言特性之前。那个时候,大家用一种和现在完全不同的方式来引用函数:第一个参数我们要传入一个对象,第二个参数我们要传入一个需要被调用的方法的名称。

如果这样就把你吓坏了,恐怕更糟的在后面。你看,这两个参数都各自有其复杂性:

  • 我们传入的对象必须是一个 class 类型,必须继承自 NSObject,也就是说不能指向某个 SwiftUI 视图结构体。
  • 方法是以方法名称的方式提供的,并非实际的方法。这个方法名被 Objective-C 用于在运行时查找实际的代码。这个方法需要拥有某个特定的签名(参数列表),否则我们的代码会不起作用。

除此之外,出于性能的考虑,Swift 倾向于不以 Objective-C 可以读取的方式生成代码 —— “在运行时查找方法” 这套东西虽然优雅,但真的很慢。所以,当我们要书写这个方法名时,我们需要做两件事:

  1. 用一个叫 #selector 的特殊编译器指令标记方法名,这个动作会要求 Swift 确保我们的方法存在。
  2. 给目标方法添加一个 @objc 注解,这会告诉 Swift 生成 Objective-C 可以读取的方法。

你看,我在切换到 SwiftUI 之前已经写了近十年的 UIKit 代码了。即便如此,还是费了这么大劲才解释清楚这个旧 API 是怎么回事。对比之下,它就像一个反人性的罪犯。但现状就是这样,我们无法摆脱。

在我展示代码之前,我还要提一下第四个参数。第一个是要保存的图片,第二是要被通知保存结果的对象,第三个是对象的某个方法,第四个呢?这里我们不会直接使用它,但你需要了解它的用途:我们可以传递任何数据给这个参数,然后它会在保存完成方法被调用时回传给我们。

在 UIKit 中这被称为 “上下文”,它能帮助识别不同的图片保存操作。字面上,你可以提供任何东西给这个参数,所以 UIKit 采用了你可以想象到的最自由的类型:一块 Swift 不做任何保障的原始内存块。它在 Swift 中有自己专门的类型:UnsafeRawPointer。老实说,如果不是因为这里需要用到,我甚至都不会告诉你它的存在。现阶段,这个类型对你的应用开发没什么用处。

言归正传,让我们搞定最后的步骤,然后真正开始这个项目。

如我提到的,写入图片到相册并且读取响应,需要用到一个继承自 NSObject 的类。在这个类里面,我们需要写一个用 @objc 标记的方法。有了这两样东西,我们就可以用它们来调用 UIImageWriteToSavedPhotosAlbum() 了。

把拼图都放到一块,在 ContentView 外面添加下面这个类:

class ImageSaver: NSObject {
    func writeToPhotoAlbum(image: UIImage) {
        UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveError), nil)
    }

    @objc func saveError(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        print("Save finished!")
    }
}复制代码

然后在 SwiftUI 里使用:

let imageSaver = ImageSaver()
imageSaver.writeToPhotoAlbum(image: inputImage)复制代码

运行代码,在选择一张图片后,你应该会在输出窗口看到 “Save finished!” 的消息输出。是的,代码虽少,但是我们解释了很多。我们花了这么长的时间,终于完成了项目的技术概览。接下来,请把项目恢复默认状态。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~