寫一個功能完善的圖片瀏覽器
最終效果 源碼
本篇文章后完成的效果
前言: 在之前我們已經實現了對單張圖片的手勢處理縮放,如果順利的話 , 我們的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.png
這里在設置了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鼓勵一下, 歡迎關注