iOS实现相册和网络图片的存取
保存 uiimage 到相册
uikit
uikit 中一个古老的方法,objective-c 的形式
void uiimagewritetosavedphotosalbum(uiimage *image, id completiontarget, sel completionselector, void *contextinfo);
保存完成后,会调用 completiontarget 的 completionselector。如果 completiontarget 不为空,completiontarget 必须实现以下方法
- (void)image:(uiimage *)image didfinishsavingwitherror:(nserror *)error contextinfo:(void *)contextinfo;
objective-c 的写法
- (void)saveimage:(uiimage *)image { uiimagewritetosavedphotosalbum(image, self, @selector(image:didfinishsavingwitherror:contextinfo:), nil); } - (void)image:(uiimage *)image didfinishsavingwitherror:(nserror *)error contextinfo:(void *)contextinfo { if (error) { // fail } else { // success } }
swift 的写法
func saveimage(_ image: uiimage) { uiimagewritetosavedphotosalbum(image, self, #selector(image(_:didfinishsavingwitherror:contextinfo:)), nil) } func image(_ image: uiimage, didfinishsavingwitherror error: nserror?, contextinfo: anyobject) { if error == nil { // success } else { // fail } }
photos framework
ios 8 开始,可以用 photos framework。phassetchangerequest 的类方法可以保存 uiimage
class func creationrequestforasset(from image: uiimage) -> self
编辑相册需要在 phphotolibrary 的闭包中进行,有两种方法
func performchanges(_ changeblock: @escaping () -> void, completionhandler: ((bool, error?) -> void)? = nil)
func performchangesandwait(_ changeblock: @escaping () -> void) throws
以上两种方法,分别是异步和同步执行。一般用第一种异步执行的方法,不会阻塞主线程。
func saveimage(_ image: uiimage) { phphotolibrary.shared().performchanges({ phassetchangerequest.creationrequestforasset(from: image) }, completionhandler: { (success, error) in // not on main thread if success { // success } else if let error = error { // handle error } }) }
编辑相册的闭包 changeblock 和完成的闭包 completionhandler,是在 serial queue 中执行,不在主线程。需要更新 ui 的话,要切换到主线程中执行。
保存图片的 data 到相册
如果有图片的数据(data 或 nsdata),可以用 photos framework 的方法保存到相册。从 ios 9 开始,可以使用 phassetcreationrequest 的方法
func addresource(with type: phassetresourcetype, data: data, options: phassetresourcecreationoptions?)
ios 8 比较麻烦,需要把数据写入临时文件,用临时文件的 url 作为参数,调用 phassetchangerequest 的类方法
class func creationrequestforassetfromimage(atfileurl fileurl: url) -> self?
以下是兼容 ios 8 的写法
func saveimagedata(_ data: data) { if #available(ios 9.0, *) { phphotolibrary.shared().performchanges({ phassetcreationrequest.forasset().addresource(with: .photo, data: data, options: nil) }, completionhandler: { (success, error) in // not on main thread if success { // success } else if let error = error { // handle error } }) } else { // write image data to temp file let temppath = nstemporarydirectory().appending("tempimagetosavetophoto.image") let tempurl = url(fileurlwithpath: temppath) try? data.write(to: tempurl) phphotolibrary.shared().performchanges({ phassetchangerequest.creationrequestforassetfromimage(atfileurl: tempurl) }, completionhandler: { (success, error) in // not on main thread if success { // success } else if let error = error { // handle error } // remove temp file try? filemanager.default.removeitem(at: tempurl) }) } }
sdwebimage 缓存 uiimage、data
sdwebimage (目前版本 4.0.0) 有两个方法可以使用。
sdwebimagemanager 的方法
- (void)saveimagetocache:(nullable uiimage *)image forurl:(nullable nsurl *)url;
sdimagecache 的方法
- (void)storeimage:(nullable uiimage *)image imagedata:(nullable nsdata *)imagedata forkey:(nullable nsstring *)key todisk:(bool)todisk completion:(nullable sdwebimagenoparamsblock)completionblock;
这个方法的 image、key 参数不能为空,否则直接执行 completionblock 就返回。
从相册获取 uiimage、data
uiimagepickercontroller 是常用的照片选取控制器。实现一个代理方法即可
optional func imagepickercontroller(_ picker: uiimagepickercontroller, didfinishpickingmediawithinfo info: [string : any])
通过 info 字典,可以获取 uiimage 等信息。这里用来查询 info 字典的 key 有
uiimagepickercontrolleroriginalimage // 原始 uiimage uiimagepickercontrollereditedimage // 编辑后的 uiimage uiimagepickercontrollerreferenceurl // alasset 的 url
通过 alasset 的 url 可获取 phasset。通过 phimagemanager 的方法可以获得相册图片的 data
func requestimagedata(for asset: phasset, options: phimagerequestoptions?, resulthandler: @escaping (data?, string?, uiimageorientation, [anyhashable : any]?) -> void) -> phimagerequestid
以下是代码示例
func imagepickercontroller(_ picker: uiimagepickercontroller, didfinishpickingmediawithinfo info: [string : any]) { picker.dismiss(animated: true, completion: nil) if let image = info[uiimagepickercontrolleroriginalimage] as? uiimage { // get original image } if let url = info[uiimagepickercontrollerreferenceurl] as? url, let asset = phasset.fetchassets(withalasseturls: [url], options: nil).firstobject { phimagemanager.default().requestimagedata(for: asset, options: nil, resulthandler: { (imagedata, _, _, _) in if let data = imagedata { // get image data } }) } }
从 sdwebimage 的缓存中获取 uiimage、data
sdwebimage 给 uiimageview 提供了方法,方便获取、显示网络图片。如果需要获取下载的图片(进行保存到相册、上传至服务器等操作),可以用以下方法
- (nullable id <sdwebimageoperation>)loadimagewithurl:(nullable nsurl *)url options:(sdwebimageoptions)options progress:(nullable sdwebimagedownloaderprogressblock)progressblock completed:(nullable sdinternalcompletionblock)completedblock;
swift 的代码示例
sdwebimagemanager.shared().loadimage(with: url, options: sdwebimageoptions(rawvalue: 0), progress: nil, completed: { [weak self] (cachedimage, imagedata, error, _, _, _) in guard self != nil else { return } if let image = cachedimage { // get image } if let data = imagedata { // get image data } if error != nil { // handle error } })
这个方法有个问题,对于静态图片,可能获取不到 data。如果需要获取图片 data 的话,不能直接这么写。查看源码可以找到原因。sdwebimagemanager 的 loadimage: 方法会调用 sdimagecache 的 querycacheoperationforkey: 方法
diskimagedatabysearchingallpathsforkey: 方法用来获取 disk 中图片的 data。当图片在 memory 中,只有 gif 图片才会提供 data,静态图的 data 为空;当图片在 disk 中,都会提供 data。如果能在外部直接调用 diskimagedatabysearchingallpathsforkey: 方法就很简单,但是不行,这是私有方法,只写在 .m 文件里,对外不可见。
改源码可以解决问题,将上图第一个箭头的 if 判断去掉,总是调用 diskimagedatabysearchingallpathsforkey: 方法。然而,改第三方库源码不好,可能会有想不到的糟糕后果。
一种方法是,根据 diskimageexistswithkey: 方法,获取 disk 上的 data。
判断 disk 的图片是否存在,就是查找两个路径。同样,拿到这两个路径的文件就可以获得 data。以下是 swift 代码示例
sdwebimagemanager.shared().diskimageexists(for: imageurl) { [weak self] (exist) in // always on main thread guard self != nil else { return } if exist { // find image data from disk var data: nsdata? // get cache key let key = sdwebimagemanager.shared().cachekey(for: imageurl) // get cache path if let path = sdimagecache.shared().defaultcachepath(forkey: key) { data = nsdata(contentsoffile: path) if data == nil { data = nsdata(contentsoffile: (path as nsstring).deletingpathextension) } } if data != nil { // get image data } else { // fail getting image data } } else { // no disk image } }
这个方法缺点在于,代码复杂,可能会在 sdwebimage 版本升级后失效(例如,disk 缓存路径改变)。
推荐的方法是,将图片缓存从 memory 中移除,然后调用 sdwebimagemanager 的 loadimage: 方法。
// get cache key let key = sdwebimagemanager.shared().cachekey(for: imageurl) // remove memory cache sdimagecache.shared().removeimage(forkey: key, fromdisk: false, withcompletion: nil) // load image and data sdwebimagemanager.shared().loadimage(with: imageurl, options: sdwebimageoptions(rawvalue: 0), progress: nil) { [weak self] (_, data, _, _, _, _) in guard self != nil else { return } if data != nil { // get image data } else { // fail getting image data } }
这样写比较简洁。即使 sdwebimage 版本升级后改变 disk 缓存路径,依然有效。以上代码执行之后,当前图片又会存在 memory 中。
遗留问题
将 jpg 图片的 data 保存至相册,然后再取出的 data 与保存的 data 可能不一样。requestimagedata: 方法传入 phimagerequestoptions,phimagerequestoptions 的 version 试了三种值(current、unadjusted、original)都不行。png、gif 图片还没遇到这个问题。可能保存 jpg 图片的过程会修改原始数据。如何使存取的数据一致?欢迎交流!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。