寫(xiě)一個(gè)功能完善的圖片瀏覽器
最終效果 源碼
本篇文章后完成的效果
前言: 在之前我們已經(jīng)實(shí)現(xiàn)了對(duì)單張圖片的手勢(shì)處理縮放,如果順利的話 , 我們的photoView目前對(duì)于單張圖片的功能支持已經(jīng)很好了, 接下來(lái)我們來(lái)實(shí)現(xiàn)對(duì)多張圖片的瀏覽, 實(shí)現(xiàn)真正意義上的圖片瀏覽器, 同時(shí)會(huì)用到kingfisher(實(shí)現(xiàn)了和SDWebImage相似的功能的一個(gè)加載圖片的swift框架 -- 王巍寫(xiě))來(lái)加載網(wǎng)絡(luò)圖片
分析
1.實(shí)現(xiàn)多張圖片的滾動(dòng)瀏覽, 相信大家都會(huì)有思路怎么實(shí)現(xiàn)的, 因?yàn)榫褪窍喈?dāng)于一個(gè)圖片輪播器 , 而且還不需要自動(dòng)滾動(dòng), 甚至不需要實(shí)現(xiàn)循環(huán)滾動(dòng), 那么應(yīng)該是很簡(jiǎn)單就能實(shí)現(xiàn)的
使用UIScrollView來(lái)實(shí)現(xiàn)圖片的滾動(dòng), 那么在這個(gè)過(guò)程中就需要注意到循環(huán)利用ImageView的處理,否則會(huì)浪費(fèi)很多的內(nèi)存容易造成內(nèi)存爆滿, 你可以使用兩個(gè)或者三個(gè)ImageView來(lái)實(shí)現(xiàn), 具體的思路分析和實(shí)現(xiàn)可以參考這里, 或者參考MJPhotoBroswer自己來(lái)管理一個(gè)ImageView的重用機(jī)制
從上面使用UIScrollView的分析中感覺(jué)到,要手動(dòng)來(lái)實(shí)現(xiàn)重用還是需要做不少的工作, 這里筆者希望比較簡(jiǎn)單高效的實(shí)現(xiàn)PhotoBroswer, 所以選擇了使用UICollectionView來(lái)實(shí)現(xiàn), 因?yàn)樗詭в兄赜脵C(jī)制, 我們可以直接拿來(lái)使用, 如果不是很熟悉collectionView的使用,也不用太擔(dān)心, 本次不會(huì)用到它的很多高級(jí)的功能, 不過(guò)后面會(huì)提到一點(diǎn)collectionView的分頁(yè)使用技巧
2 . 實(shí)現(xiàn)網(wǎng)絡(luò)圖片的加載
- 其實(shí)要很簡(jiǎn)單加載服務(wù)器的圖片, 使用apple提供給我們的一些API就可以很簡(jiǎn)單把圖片"加載"處理, 不過(guò)需要注意的是我們提到的只是能夠"加載"出來(lái), 但是其中還有很多的細(xì)節(jié)需要處理, 比如,
a.你應(yīng)該考慮異步加載圖片不要阻塞主線程, 那么當(dāng)有多張圖片的時(shí)候,你需要處理多個(gè)線程的開(kāi)銷(xiāo)和效率.
b. 對(duì)于加載完成的圖片你應(yīng)該考慮緩存, 以便于之后能很快加載, 那么緩存你需要處理"內(nèi)存緩存"(臨時(shí)緩存到內(nèi)存,加載速度很快, 但是緩存多張圖片到內(nèi)存的時(shí)候會(huì)消耗大量的內(nèi)存, 所以需要管理緩存到內(nèi)存的文件大小, 及時(shí)清空緩存)和"磁盤(pán)緩存"(持久化保存在沙盒)
相信僅僅是上面提到兩點(diǎn), 一定會(huì)讓大多數(shù)的讀者感覺(jué)到很難實(shí)現(xiàn)(是的, 鑒于筆者也感覺(jué)到自己實(shí)現(xiàn)這個(gè)網(wǎng)絡(luò)圖片的加載的困難和自己的能力有限),所以我們應(yīng)該考慮其他的方便的方法來(lái)實(shí)現(xiàn), 當(dāng)然上面給出了自己實(shí)現(xiàn)的思路, 有能力的朋友不妨自己去實(shí)現(xiàn)一下
- 既然自己實(shí)現(xiàn)很麻煩, 就找第三方來(lái)幫忙了, 這里使用到了"王巍"寫(xiě)的"kingfisher", 這個(gè)純swift的圖片加載庫(kù)提供了和SDWebImage相類(lèi)似的接口使用很是方便, 同時(shí)很驚訝的是這個(gè)框架較好的實(shí)現(xiàn)了GIF圖片的加載(關(guān)于GIF圖片的加載后面可能會(huì)提到怎么去實(shí)現(xiàn))
注意, 在使用kingfisher加載多張網(wǎng)絡(luò)圖片的時(shí)候, 你可能會(huì)注意到, xcode上面顯示的內(nèi)存消耗是很大的, 在實(shí)現(xiàn)PhotoBrowser的時(shí)候, 我使用SDWebImage和Kingfisher加載了相同的圖片, 發(fā)現(xiàn)在xcode上面顯示的內(nèi)存消耗兩者確實(shí)是相差很大的, 你會(huì)明顯的發(fā)現(xiàn)kingfisher消耗了比SDWebImage多很多的內(nèi)存, 所以筆者當(dāng)時(shí)去打擾了一下王巍, 他說(shuō)到這兩個(gè)框架的實(shí)現(xiàn)思路是相似的, 內(nèi)存消耗上不應(yīng)該有很大的區(qū)別, 可能是xcode自身顯示的bug, 后來(lái)我用真機(jī)測(cè)試多張圖片確實(shí)是沒(méi)有收到內(nèi)存警告, 所以大家可以放心的使用
思路寫(xiě)的比較啰嗦, 下面進(jìn)入實(shí)現(xiàn)部分
1). 自定義UICollectionViewCell用于展示每一張圖片
- 新建文件PhotoViewCell, 這里直接把之前的PhotoView的代碼拿過(guò)來(lái)稍作改變就可以利用之前詳細(xì)寫(xiě)一個(gè)功能完善的PhotoBrowser同時(shí)支持GIF(一)里寫(xiě)的對(duì)單張圖片的處理, 因?yàn)橹皇菗Q了一個(gè)容器而已
-
新建文件PhotoModel來(lái)作為圖片模型, 因?yàn)槊恳粋€(gè)cell顯示一張圖片, 所以它擁有這張圖片的photoModel
photoModel.png
這里在設(shè)置了photoModel的時(shí)候, 我么利用屬性觀察器來(lái)設(shè)置image
private func setupImage() {
// 首先判斷是否正確設(shè)置了photoModel
guard let photo = photoModel else {
assert(false, "設(shè)置的圖片模型不正確")
return
}
// 如果是加載本地的圖片, 直接設(shè)置圖片即可, 注意這里是photoBrowser需要提升的地方
// 因?yàn)閷?duì)于本地圖片的加載沒(méi)有做處理, 所以當(dāng)直接使用 UIImage(named"")的形式加載圖片的時(shí)候, 會(huì)消耗大量的內(nèi)存
// 不過(guò)鑒于參考了其他的圖片瀏覽器框架, 大家對(duì)本地圖片都沒(méi)有處理, 因?yàn)檫@個(gè)確實(shí)用的很少, 畢竟都是用來(lái)加載網(wǎng)絡(luò)圖片的情況比較多
// 如果發(fā)現(xiàn)確實(shí)需要處理后面會(huì)努力處理這個(gè)問(wèn)題
if photo.localImage != nil {
// 注意這個(gè)image的屬性觀察器中, 我處理了imageView的frame
image = photo.localImage
// 加載完成后直接返回
return
}
// 加載網(wǎng)路圖片, 首先判斷url是否合法
guard let urlString = photo.imageUrlString, let url = NSURL(string: urlString) else {
assert(false, "設(shè)置的url不合法")
return
}
// 設(shè)置默認(rèn)圖片
if let sourceImageView = photo.sourceImageView {
image = sourceImageView.image
}
// 如果沒(méi)有提供默認(rèn)的圖片, 就設(shè)置一張默認(rèn)的圖片
image = image ?? UIImage(named: "2")
// 這里使用kingfisher來(lái)加載網(wǎng)絡(luò)圖片 很簡(jiǎn)單的調(diào)用
imageView.kf_setImageWithURL(url, placeholderImage: image, optionsInfo: nil, progressBlock: {[weak self] (receivedSize, totalSize) in
let progress = Double(receivedSize) / Double(totalSize)
// print(progress)
// 這里面能夠獲取到加載進(jìn)度, 便于提供進(jìn)度條顯示
}) {[weak self] (image, error, cacheType, imageURL) in
// 加載完成
// 注意: 因?yàn)檫@個(gè)閉包是多線程調(diào)用的 所以可能存在 沒(méi)有顯示完圖片,就點(diǎn)擊了返回
// 這個(gè)時(shí)候self已經(jīng)被銷(xiāo)毀了 所以使用[unonwed self] 將會(huì)導(dǎo)致"野指針"的問(wèn)題
// 使用 [weak self] 保證安全訪問(wèn)self
// 但是這也不是絕對(duì)安全的, 比如在 self 銷(xiāo)毀之前, 進(jìn)入了這個(gè)閉包 那么strongSelf 有值 進(jìn)入
// 如果在這時(shí)恰好 self 銷(xiāo)毀了,那么之后調(diào)用strongSelf 都將會(huì)出錯(cuò)crash
// 可以考慮使用withExtendedLifetime
// withExtendedLifetime(self, { () -> self in
//
// })
if let strongSelf = self {
// 加載完成, 設(shè)置圖片, 觸發(fā)里面的屬性觀察器設(shè)置imageView
strongSelf.image = image
if let _ = image { return }
// 提示加載錯(cuò)誤
}
}
}
- 新建文件PhotoBroswer來(lái)處理多張圖片的顯示
- 設(shè)置collection view
private lazy var collectionView: UICollectionView = {[unowned self] in
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .Horizontal
// 每個(gè)cell的尺寸 -- 寬度設(shè)置為UICollectionView.bounds.size.width ---> 滾一頁(yè)就是一個(gè)完整的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
// 分頁(yè)每次滾動(dòng) 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
// 避免出現(xiàn)重用出錯(cuò)的問(wèn)題, 大家可以試下注釋這行會(huì)帶來(lái)什么不想要的效果, 然后應(yīng)該就理解了這個(gè)方法為何存在
cell.resetUI()
let currentModel = photoModels[indexPath.row]
// 注意之前直接傳了self的一個(gè)函數(shù)給singleTapAction 造成了循環(huán)引用
cell.singleTapAction = {[unowned self](ges: UITapGestureRecognizer) in
self.dismiss()
}
return cell
}
// 這里監(jiān)控collectionView的滾動(dòng), 是希望在滾動(dòng)超過(guò)一半的時(shí)候改更改圖片的索引, 這個(gè)會(huì)在之后的toolBar上使用到, 來(lái)顯示索引
func scrollViewDidScroll(scrollView: UIScrollView) {
// 向下取整
currentIndex = Int(scrollView.contentOffset.x / scrollView.zj_width + 0.5)
}
- 前面提到的處理collection view的分頁(yè)的一點(diǎn)小技巧
- 如果你只是簡(jiǎn)單設(shè)置了collectionView.pagingEnabled = true,設(shè)置 flowLayout.itemSize = CGSize(width: self.zj_width , height: self.zj_height), 并且設(shè)置cell里面的scrollView和contentView的尺寸相同, 那么滾動(dòng)的效果是這樣的
我們希望兩張圖片之間有一定的間隙, 那么很直接, 直接將cell里的scrollView的寬度減少一點(diǎn)應(yīng)該就可以了
/// 懶加載
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))
這樣圖片之間的間隙自然是出來(lái)的, 但是發(fā)現(xiàn)滾動(dòng)完成后后面的圖片顯示不正常. 因?yàn)閏ollectionView每次滾動(dòng)一頁(yè)的寬度是UICollectionView.bounds.size.width, 所以和cell的尺寸沒(méi)有關(guān)系, 那么我們?cè)偬幚硪幌?/p>
// 每個(gè)cell的尺寸 -- 寬度設(shè)置為UICollectionView.bounds.size.width ---> 滾一頁(yè)就是一個(gè)完整的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))
// 分頁(yè)每次滾動(dòng) 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)
到目前為止, 多張圖片的顯示以及網(wǎng)絡(luò)圖片的加載處理基本就完整了,一個(gè)比較成型的PhotoBrowser就完成了, 至于里面的toolBar和提示框, 還有過(guò)渡動(dòng)畫(huà)可能會(huì)在以后寫(xiě)寫(xiě),歡迎關(guān)注
詳細(xì)請(qǐng)移步源碼,里面都有詳細(xì)的Demo使用示例 如果您覺(jué)得有幫助,不妨給個(gè)star鼓勵(lì)一下, 歡迎關(guān)注