写一个功能完善的图片浏览器
最终效果 源码
本篇文章后完成的效果
前言: 在之前我们已经实现了对单张图片的手势处理缩放,如果顺利的话 , 我们的photoView目前对于单张图片的功能支持已经很好了, 接下来我们来实现对多张图片的浏览, 实现真正意义上的图片浏览器, 同时会用到kingfisher(实现了和SDWebImage相似的功能的一个加载图片的swift框架 -- 王巍写)来加载网络图片
分析
1.实现多张图片的滚动浏览, 相信大家都会有思路怎么实现的, 因为就是相当于一个图片轮播器 , 而且还不需要自动滚动, 甚至不需要实现循环滚动, 那么应该是很简单就能实现的
使用UIScrollView来实现图片的滚动, 那么在这个过程中就需要注意到循环利用ImageView的处理,否则会浪费很多的内存容易造成内存爆满, 你可以使用两个或者三个ImageView来实现, 具体的思路分析和实现可以参考这里, 或者参考MJPhotoBroswer自己来管理一个ImageView的重用机制
从上面使用UIScrollView的分析中感觉到,要手动来实现重用还是需要做不少的工作, 这里笔者希望比较简单高效的实现PhotoBroswer, 所以选择了使用UICollectionView来实现, 因为它自带有重用机制, 我们可以直接拿来使用, 如果不是很熟悉collectionView的使用,也不用太担心, 本次不会用到它的很多高级的功能, 不过后面会提到一点collectionView的分页使用技巧
2 . 实现网络图片的加载
- 其实要很简单加载服务器的图片, 使用apple提供给我们的一些API就可以很简单把图片"加载"处理, 不过需要注意的是我们提到的只是能够"加载"出来, 但是其中还有很多的细节需要处理, 比如,
a.你应该考虑异步加载图片不要阻塞主线程, 那么当有多张图片的时候,你需要处理多个线程的开销和效率.
b. 对于加载完成的图片你应该考虑缓存, 以便于之后能很快加载, 那么缓存你需要处理"内存缓存"(临时缓存到内存,加载速度很快, 但是缓存多张图片到内存的时候会消耗大量的内存, 所以需要管理缓存到内存的文件大小, 及时清空缓存)和"磁盘缓存"(持久化保存在沙盒)
相信仅仅是上面提到两点, 一定会让大多数的读者感觉到很难实现(是的, 鉴于笔者也感觉到自己实现这个网络图片的加载的困难和自己的能力有限),所以我们应该考虑其他的方便的方法来实现, 当然上面给出了自己实现的思路, 有能力的朋友不妨自己去实现一下
- 既然自己实现很麻烦, 就找第三方来帮忙了, 这里使用到了"王巍"写的"kingfisher", 这个纯swift的图片加载库提供了和SDWebImage相类似的接口使用很是方便, 同时很惊讶的是这个框架较好的实现了GIF图片的加载(关于GIF图片的加载后面可能会提到怎么去实现)
注意, 在使用kingfisher加载多张网络图片的时候, 你可能会注意到, xcode上面显示的内存消耗是很大的, 在实现PhotoBrowser的时候, 我使用SDWebImage和Kingfisher加载了相同的图片, 发现在xcode上面显示的内存消耗两者确实是相差很大的, 你会明显的发现kingfisher消耗了比SDWebImage多很多的内存, 所以笔者当时去打扰了一下王巍, 他说到这两个框架的实现思路是相似的, 内存消耗上不应该有很大的区别, 可能是xcode自身显示的bug, 后来我用真机测试多张图片确实是没有收到内存警告, 所以大家可以放心的使用
思路写的比较啰嗦, 下面进入实现部分
1). 自定义UICollectionViewCell用于展示每一张图片
- 新建文件PhotoViewCell, 这里直接把之前的PhotoView的代码拿过来稍作改变就可以利用之前详细写一个功能完善的PhotoBrowser同时支持GIF(一)里写的对单张图片的处理, 因为只是换了一个容器而已
-
新建文件PhotoModel来作为图片模型, 因为每一个cell显示一张图片, 所以它拥有这张图片的photoModel
这里在设置了photoModel的时候, 我么利用属性观察器来设置image
private func setupImage() {
// 首先判断是否正确设置了photoModel
guard let photo = photoModel else {
assert(false, "设置的图片模型不正确")
return
}
// 如果是加载本地的图片, 直接设置图片即可, 注意这里是photoBrowser需要提升的地方
// 因为对于本地图片的加载没有做处理, 所以当直接使用 UIImage(named"")的形式加载图片的时候, 会消耗大量的内存
// 不过鉴于参考了其他的图片浏览器框架, 大家对本地图片都没有处理, 因为这个确实用的很少, 毕竟都是用来加载网络图片的情况比较多
// 如果发现确实需要处理后面会努力处理这个问题
if photo.localImage != nil {
// 注意这个image的属性观察器中, 我处理了imageView的frame
image = photo.localImage
// 加载完成后直接返回
return
}
// 加载网路图片, 首先判断url是否合法
guard let urlString = photo.imageUrlString, let url = NSURL(string: urlString) else {
assert(false, "设置的url不合法")
return
}
// 设置默认图片
if let sourceImageView = photo.sourceImageView {
image = sourceImageView.image
}
// 如果没有提供默认的图片, 就设置一张默认的图片
image = image ?? UIImage(named: "2")
// 这里使用kingfisher来加载网络图片 很简单的调用
imageView.kf_setImageWithURL(url, placeholderImage: image, optionsInfo: nil, progressBlock: {[weak self] (receivedSize, totalSize) in
let progress = Double(receivedSize) / Double(totalSize)
// print(progress)
// 这里面能够获取到加载进度, 便于提供进度条显示
}) {[weak self] (image, error, cacheType, imageURL) in
// 加载完成
// 注意: 因为这个闭包是多线程调用的 所以可能存在 没有显示完图片,就点击了返回
// 这个时候self已经被销毁了 所以使用[unonwed self] 将会导致"野指针"的问题
// 使用 [weak self] 保证安全访问self
// 但是这也不是绝对安全的, 比如在 self 销毁之前, 进入了这个闭包 那么strongSelf 有值 进入
// 如果在这时恰好 self 销毁了,那么之后调用strongSelf 都将会出错crash
// 可以考虑使用withExtendedLifetime
// withExtendedLifetime(self, { () -> self in
//
// })
if let strongSelf = self {
// 加载完成, 设置图片, 触发里面的属性观察器设置imageView
strongSelf.image = image
if let _ = image { return }
// 提示加载错误
}
}
}
- 新建文件PhotoBroswer来处理多张图片的显示
- 设置collection view
private lazy var collectionView: UICollectionView = {[unowned self] in
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .Horizontal
// 每个cell的尺寸 -- 宽度设置为UICollectionView.bounds.size.width ---> 滚一页就是一个完整的cell
flowLayout.itemSize = CGSize(width: self.zj_width + PhotoBrowser.contentMargin, height: self.zj_height)
flowLayout.minimumLineSpacing = 0.0
flowLayout.minimumInteritemSpacing = 0.0
flowLayout.sectionInset = UIEdgeInsetsZero
// 分页每次滚动 UICollectionView.bounds.size.width
let collectionView = UICollectionView(frame: CGRect(x: 0.0, y: 0.0, width: self.zj_width + PhotoBrowser.contentMargin, height: self.zj_height), collectionViewLayout: flowLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.pagingEnabled = true
collectionView.registerClass(PhotoViewCell.self, forCellWithReuseIdentifier: PhotoBrowser.cellID)
self.insertSubview(collectionView, atIndex: 0)
return collectionView
}()
- 处理collection view的代理和datasource方法, 这里面比较容易理解
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoModels.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(PhotoBrowser.cellID, forIndexPath: indexPath) as! PhotoViewCell
// 避免出现重用出错的问题, 大家可以试下注释这行会带来什么不想要的效果, 然后应该就理解了这个方法为何存在
cell.resetUI()
let currentModel = photoModels[indexPath.row]
// 注意之前直接传了self的一个函数给singleTapAction 造成了循环引用
cell.singleTapAction = {[unowned self](ges: UITapGestureRecognizer) in
self.dismiss()
}
return cell
}
// 这里监控collectionView的滚动, 是希望在滚动超过一半的时候改更改图片的索引, 这个会在之后的toolBar上使用到, 来显示索引
func scrollViewDidScroll(scrollView: UIScrollView) {
// 向下取整
currentIndex = Int(scrollView.contentOffset.x / scrollView.zj_width + 0.5)
}
- 前面提到的处理collection view的分页的一点小技巧
- 如果你只是简单设置了collectionView.pagingEnabled = true,设置 flowLayout.itemSize = CGSize(width: self.zj_width , height: self.zj_height), 并且设置cell里面的scrollView和contentView的尺寸相同, 那么滚动的效果是这样的
我们希望两张图片之间有一定的间隙, 那么很直接, 直接将cell里的scrollView的宽度减少一点应该就可以了
/// 懒加载
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: CGRect(x: 0.0, y: 0.0, width: self.contentView.zj_width - PhotoBrowser.contentMargin, height: self.contentView.zj_height))
这样图片之间的间隙自然是出来的, 但是发现滚动完成后后面的图片显示不正常. 因为collectionView每次滚动一页的宽度是UICollectionView.bounds.size.width, 所以和cell的尺寸没有关系, 那么我们再处理一下
// 每个cell的尺寸 -- 宽度设置为UICollectionView.bounds.size.width ---> 滚一页就是一个完整的cell
flowLayout.itemSize = CGSize(width: self.zj_width + PhotoBrowser.contentMargin, height: self.zj_height)
/// cell中scrollView的尺寸
let scrollView = UIScrollView(frame: CGRect(x: 0.0, y: 0.0, width: self.contentView.zj_width - PhotoBrowser.contentMargin, height: self.contentView.zj_height))
// 分页每次滚动 UICollectionView.bounds.size.width
let collectionView = UICollectionView(frame: CGRect(x: 0.0, y: 0.0, width: self.zj_width + PhotoBrowser.contentMargin, height: self.zj_height), collectionViewLayout: flowLayout)
到目前为止, 多张图片的显示以及网络图片的加载处理基本就完整了,一个比较成型的PhotoBrowser就完成了, 至于里面的toolBar和提示框, 还有过渡动画可能会在以后写写,欢迎关注
详细请移步源码,里面都有详细的Demo使用示例 如果您觉得有帮助,不妨给个star鼓励一下, 欢迎关注