详细写一个功能完善的PhotoBrowser(二)加载多张图片以及加载网络图片

写一个功能完善的图片浏览器

最终效果 源码

网络.gif

本地图片.gif

本篇文章后完成的效果

本篇效果.gif

前言: 在之前我们已经实现了对单张图片的手势处理缩放,如果顺利的话 , 我们的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 }
                // 提示加载错误
            }
        }
    }
  1. 新建文件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)

    }
  1. 前面提到的处理collection view的分页的一点小技巧
  • 如果你只是简单设置了collectionView.pagingEnabled = true,设置 flowLayout.itemSize = CGSize(width: self.zj_width , height: self.zj_height), 并且设置cell里面的scrollView和contentView的尺寸相同, 那么滚动的效果是这样的
没有间隙.png

我们希望两张图片之间有一定的间隙, 那么很直接, 直接将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))
间隙.gif

这样图片之间的间隙自然是出来的, 但是发现滚动完成后后面的图片显示不正常. 因为collectionView每次滚动一页的宽度是UICollectionView.bounds.size.width, 所以和cell的尺寸没有关系, 那么我们再处理一下

分页分析.png
        // 每个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鼓励一下, 欢迎关注


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 因为要结局swift3.0中引用snapKit的问题,看到一篇介绍Xcode8,swift3变化的文章,觉得很详细...
    uniapp阅读 4,385评论 0 12
  • 前言 iOS里的UI控件其实没有几个,界面基本就是围绕那么几个控件灵活展开,最难的应属UICollectionVi...
    alenpaulkevin阅读 31,341评论 9 175
  • 昨天晚上突如其来的生病,突然感到自己好脆弱,在那种特别疼痛的状态下,心里打翻了五味瓶,不是滋味,去急诊的路上 心里...
    昊子_011c阅读 192评论 0 0
  • 本人党辉,生于1974年,父亲是一名军转干部从小就继承了不屈不挠不服输的军人精神。在不断打拼的道路上一直激励着我。...
    党义强阅读 394评论 0 1
  • 这也是多年前的事情了,我每次想起来都莫名的伤感。 当时工作在上海,压力不大,薪水不高,工作轻松。上海当时ADSL刚...
    花开遍野阅读 282评论 0 2