iOS 图片相册保存实践

swift 发布于 2022年07月06日
保存照片到系统相册

在我们开发 iOS APP 的时候,多少会遇到这样的需求,就是保存一张或者多张图片到系统相册中。这是一个很简单的功能,并且 iOS 也在很早就为开发者提供了相应的接口调用,就是 UIImageWriteToSavedPhotosAlbum 函数。

它的函数签名如下:

func UIImageWriteToSavedPhotosAlbum(_ image: UIImage,
_ completionTarget: Any?,
_ completionSelector: Selector?,
_ contextInfo: UnsafeMutableRawPointer?)

这个函数接受三个参数,第一个参数 image 顾名思义,就是要保存到系统相册中的图片。 completionTargetcompletionSelector 参数可以指定一个接受保存完成事件的一个示例和它的方法。

contextInfo 用于提供一个上下文信息,这里面的信息会通过参数传递给 completionSelector 函数。假如同时有多处调用这个方法,这个上下文信息就有用了,可以标识出本次完成回调代表的是哪一个调用。

保存多张图片

好了,函数签名就介绍完了,本身并不复杂,如果你在搜索引擎中查找的话,可能会看到很多例子会这样写:

UIImageWriteToSavedPhotosAlbum(imageWantToSave, nil, nil, nil)

只传入了第一个 image 参数,后面三后参数都传入 nil。 这样调用在大多数情况下也都没有问题。图片正常的保存到了系统相册,一行代码完成了我们的需求,看起来方便又好用。

但在某些情况下,这个调用方式就会有问题了。比如这样:

for image in imageList {

UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)

}

这里的 imageList 是一个图片数组,会包含多张图片,如果运行这样的代码,你就会发现你的图片保存会经常出错。 比如 imageList 里面有 10 张图片,可能在有些时候只能成功保存 4 张,后面的 6 张图片既没看到报错信息,也没成功。 但在另一些时候又能成功的把这 10 张图片都保存下来。

这种介于中间态的不确定状态是最让人头疼的。说它有问题,但不是每次都失败。说它没问题,但总有一些时候会失败,而且频率还不低。甚至会引起你对系统库本身稳定性的怀疑。

其实导致这个问题的原因,就是你少传入了第 2,3 个参数。 当然,如果说是系统库稳定性的问题,也不是完全没有道理。 不过与其说是稳定性,更提贴切的说法是这个函数的接口设计不够完善,容易让大家造成误解。

第 2,3 个参数用于指定一个接收完成事件的实例和方法,可以这样:

func saveImage() {

//...
for image in imageList {

UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.imageSaveFinished(image:error:context:)), nil)

}

}

func imageSaveFinished(image: UIImage, error: Error, context: UnsafeRawPointer) {

print(error)

}

这次指定了 imageSaveFinished 方法作为 UIImageWriteToSavedPhotosAlbum 函数的回调事件。 它接受三个参数, image 表示正在保存的图片, error 代表保存过程中发生的错误, context 代表上下文信息,前面我们提到过。

imageSaveFinished 用 print 方法打印出错误信息。 再重新运行一下项目,就会在控制台上看到类似这样的输出:

There was a problem writing this asset because the writing resources are busy.

这个错误原因从字面上就可以看出来,是因为写入操作过于频繁,导致了写入失败。 因为看不到 UIImageWriteToSavedPhotosAlbum 的源码,所以它的内部机制不得而知,我们能得到的线索就是不能在同一个线程对它连续频繁调用。

从这个错误还可以得到一个信息,就是之所以会发生这个错误,是因为前面的写入操作还没有执行完成,就开始了下一个。那么我们是不是可以对调用做一个简单处理呢,等到上一个操作完成在进行下一个写入?

以现有的接口其实是可以做到的,还回到 imageSaveFinished 方法上来,我们可以在每次写入成功后,再调用 UIImageWriteToSavedPhotosAlbum 方法写入下一个图片,这样就不会发生连续写入图片导致的写入错误了:

func saveImage() {

if self.imageList.count > 0 {

if let image = self.imageList.first {

UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.imageSaveFinished(image:error:context:)), nil)

}

}

}

func imageSaveFinished(image: UIImage, error: Error, context: UnsafeRawPointer) {

self.imageList.removeFirst()

if self.imageList.count > 0 {

saveImage()

}

}

这次把 saveImage 方法改写了一下,每次调用,它只去 imageList 中第一个图片。 并且 imageList 也被声明成属性,可以跨方法访问。 然后在成功回调 imageSaveFinished 中,首先删除 imageList 中的第一张图片,也就这次保存成功的,然后判断 imageList 在删除后是否还有其他图片,如果有,那么继续调用 saveImage 方法保存。

这样,图片的保存就变成了顺序执行,只有上一张图片保存完成后,后面的图片才能开始保存。避免了频繁操作的问题。当然这里写的还是稍微简单,你还可以把它写的更健壮一些,比如在 imageSaveFinished 里面判断 error 是否为 nil 来界定回调发生时候图片是否真正的被保存成功。

但这个整体逻辑是没问题的,修改完成后在运行这个程序,你就会看到所有的批量图片保存操作都能成功的完成了。 再也不会出现之前的间歇性抽风问题了~

结束

UIImageWriteToSavedPhotosAlbum 是 iOS 提供的一个非常方便的图片存储接口,大多数情况下它的使用很简单。当然对于保存多张图片的时候,还需要进行一些额外的处理,但总体还是很方便。在 iOS 10 以后的设备上使用它还需要注意一个细节,就是你需要在 Info.plist 中声明一个权限字段:

<key>NSPhotoLibraryUsageDescription</key>
<string>保存图片</string>

这个字段用于给用户说明 APP 使用这个权限用来做什么,如果你声明这个字段,在 iOS 10 的设备上调用这个方法就会导致 Crash。


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe