Swift 3.0 商城开发 —— 图片滑动组件

效果图

image
image

知识点

  • 学习 UIScrollView 和 UIPageControl 的基本用法
  • 学习 三个 UIImageView 实现无限循环滚动思想
  • 学习 我的 UIView 常用扩展
  • 学习 给我们的视图设置代理事件

主要设计思想

概要介绍我在设计此控件的一些具体步骤与思想

  • 创建 UIScrollView 作为滚动视图的容器;
  • 创建 UIPageControl 作为显示页数以及总页数的组件
  • 在 UIScrollView 中创建三个 UIImageView 分别作为 上一页 当前页 下一页
  • 通过绑定数据 data 的 didSet 来动态部署 UIScrollView 中的 UIImageView 视图。(实现无限循环)
  • 通过 Timer 实现定时跳转到下一个视图
  • 调用 UIScrollView 扩张 extension 实现具体操作细节

教程开始

  1. UIScrollView 使用

博主习惯创建组件的方式如下:

// fileprivate 是 Swift 3.0 新增加的访问控制权限:文件内访问
// 封装控件时,我们要主动的隐藏掉具体的实现,只开放使用接口即可
fileprivate var scrollView: UIScrollView = {
    let object = UIScrollView()
    // 是否可以拉伸滑动
    object.bounces = false
    // 隐藏 水平和垂直 滚动条
    object.showsVerticalScrollIndicator = false
    object.showsHorizontalScrollIndicator = false
    // 分页
    object.isPagingEnabled = true
    return object
}()
  1. UIPageControl 使用

创建 UIPageControl 组件,构建如下:

fileprivate var pageControl: UIPageControl = {
    let object = UIPageControl()
    // 单页面下 隐藏
    object.hidesForSinglePage = true
    // 当前页背景色
    object.currentPageIndicatorTintColor = UIColor.red
    // 其他也背景色
    object.pageIndicatorTintColor = UIColor.gray
    return object
}()
  1. 主视图配置

主要包括:主视图的属性配置、添加子视图

private func prepareUI() {
    self.backgroundColor = UIColor.white
    self.scrollView.delegate = self
    // 添加 滑动试图
    self.addSubview(scrollView)
    createScrollView()
    // 添加 页面控制
    self.addSubview(pageControl)
}
private func layoutUI() {
    scrollView.frame = self.bounds
    pageControl.frame = CGRect(x: self.frame.width - 85, y: self.frame.height - 25, width: 80, height: 20)
    scrollView.contentSize = CGSize(width: CGFloat(imageCount) * viewSize.width, height: viewSize.height)
}

  1. 构建 UIScrollView 子视图

重点:通常情况下,开发者可能根据图片数组的数量在 UIScrollView 中创建对应数量的 UIImageView 扩充 UIScrollView 的 contentSize 实现所有图片的滑动;这种设计思路比较常规,但有两个问题:1.如果图片数据过大,此控件将占用过大的内存去创建视图;2.很难实现无限循环滑动。

我的思想:在 UIScrollView 中只创建三个 UIImageView 作为 上一页图片 当前页图片 下一页图片 的容器。通过判断当前页所在 图片数据中的位置,动态为三个 UIImageView 填充指定图片。

// 创建 滑动视图子视图 
func createScrollView() {
    for i in 0 ..< 3 {
        let imageView: UIImageView = {
            let object = UIImageView()
            object.isUserInteractionEnabled = true
            object.contentMode = UIViewContentMode.scaleToFill
            return object
        }()
        imageView.frame = CGRect(x: CGFloat(i) * viewSize.width, y: 0, width: viewSize.width, height: viewSize.height)
        let tap = UITapGestureRecognizer(target: self, action: #selector(touchImage))
        imageView.addGestureRecognizer(tap)
        scrollView.addSubview(imageView)
    }
}

  1. 重点:动态更新 UIImageView 视图实现无限滚动

核心知识:如果通过三个 UIImageView 实现 无限图片的 无限循环滑动:

func updateScrollView() {
    // 遍历三次
    for i in 0 ..< 3 {
        获取 UIScrollView 中 UIImageView
        let imageView = scrollView.subviews[i] as! UIImageView
        // 获取 当前展示的图片是 序号
        var index = pageControl.currentPage
        // 如果 UIImageView UIScrollView 中的第二个视图,即 上一个图片
        if i == 0 {
            // 则 index 等于 当前图片序号 - 1
            index -= 1
        }
        // 如果 UIImageView 是 UIScrollView 中的第三个视图,即下一个图片
        if i == 2 {
            // 则 index 等于 当前图片序号 + 1
            index += 1
        }
        // 越界操作 操作
        if index < 0 {
            index = pageControl.numberOfPages - 1
        }
        
        if index >= pageControl.numberOfPages {
            index = 0
        }
        imageView.tag = index
        
        if let currentData = data {
            imageView.image = UIImage(named: currentData[index].imageUrl!)
        }
    }
    // 调整 UIScrollView 偏移量 使其永远只显示中间的 UIImageView
    scrollView.contentOffset = CGPoint(x: viewSize.width, y: 0)
}

糊涂的童鞋,奉上我画的示意图

假如我们的图片数据只有 6 张图片;
红色代表原始数组,蓝色代表我们的 UIScrollView 两边越界是 填充收尾图片

image
image

最后部分

设置定时器

// MARK: Timer
fileprivate func startTimer() {
    let selector = #selector(nextImage)
    timer = Timer(timeInterval: 3.0, target: self, selector: selector, userInfo: nil, repeats: true)
    RunLoop.main.add(timer!, forMode: RunLoopMode.commonModes)
}

fileprivate func stopTimer() {
    timer?.invalidate()
    timer = nil
}

扩展完善交互时的事件

extension ImageScrollView: UIScrollViewDelegate {
    
    // 当 scrollView 有偏移量时出发
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        var page: Int = 0
        var minDistance: CGFloat = CGFloat(MAXFLOAT)
        for i in 0 ..< 3 {
            let imageView = scrollView.subviews[i] as! UIImageView
            // 由于当前页有用都是中间页,所以当 偏移量==图片宽度时,就是当前页。
            let distance:CGFloat = abs(imageView.x - scrollView.contentOffset.x)
            if distance < minDistance {
                minDistance = distance
                page = imageView.tag
            }
        }
        pageControl.currentPage = page
    }
    
    // 开始拖动 UIScrollView 事件
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        stopTimer()
    }
    
    // 结束拖动 UIScrollView 事件
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        startTimer()
    }
    
    // 
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        updateScrollView()
    }
    
    // UIScrollView 结束滚动
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        updateScrollView()
    }
    
}

学会设置代理

具体步骤:

  1. 定义代理 delegate
  2. 配置代理事件
  3. 实例对象,调用对象代理
// 1
@objc protocol ImageScrollViewDelegate {
    @objc optional func touchAt(index: Int)
}
// 2
class ImageScrollView: UIView {
    var delegate: ImageScrollViewDelegate?
    
    // 触发代理
    func touchImage(tap: UITapGestureRecognizer) {
        if let index = tap.view?.tag {
            delegate?.touchAt!(index: index)
        }
    }
    ...... }
// 3
// 实现代理:在调用 ImageScrollView 控件的 VC 中扩展,具体看源码
extension ImageScrollViewController: ImageScrollViewDelegate {
    func touchAt(index: Int) {
        print(index)
    }
}

源码

组件源码

import UIKit

@objc protocol ImageScrollViewDelegate {
    @objc optional func touchAt(index: Int)
}

class ImageScrollView: UIView {
    var delegate: ImageScrollViewDelegate?
    fileprivate var timer: Timer?
    var viewSize: CGSize!
    var data: [ImageScrollData]? {
        didSet {
            if timer != nil {
                timer!.invalidate()
                timer = nil
            }
            
            if let scrollData = data {
                pageControl.numberOfPages = scrollData.count
                pageControl.currentPage = 0
                updateScrollView()
                startTimer()
            }
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.frame = frame
        viewSize = frame.size
        prepareUI()
        layoutUI()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    private func prepareUI() {
        self.backgroundColor = UIColor.white
        self.scrollView.delegate = self
        // 添加 滑动试图
        self.addSubview(scrollView)
        createScrollView()
        // 添加 页面控制
        self.addSubview(pageControl)
    }
    
    private func layoutUI() {
        scrollView.frame = self.bounds
        pageControl.frame = CGRect(x: self.frame.width - 85, y: self.frame.height - 25, width: 80, height: 20)
        scrollView.contentSize = CGSize(width: CGFloat(3) * viewSize.width, height: viewSize.height)
    }
    
    // 创建 滑动视图子视图
    func createScrollView() {
        for i in 0 ..< 3 {
            let imageView: UIImageView = {
                let object = UIImageView()
                object.isUserInteractionEnabled = true
                object.contentMode = UIViewContentMode.scaleToFill
                return object
            }()
            imageView.frame = CGRect(x: CGFloat(i) * viewSize.width, y: 0, width: viewSize.width, height: viewSize.height)
            let tap = UITapGestureRecognizer(target: self, action: #selector(touchImage))
            imageView.addGestureRecognizer(tap)
            scrollView.addSubview(imageView)
        }
    }
    
    func updateScrollView() {
        for i in 0 ..< 3 {
            let imageView = scrollView.subviews[i] as! UIImageView
            var index = pageControl.currentPage
            if i == 0 {
                index -= 1
            }
            
            if i == 2 {
                index += 1
            }
            
            if index < 0 {
                index = pageControl.numberOfPages - 1
            }
            
            if index >= pageControl.numberOfPages {
                index = 0
            }
            imageView.tag = index
            
            if let currentData = data {
                imageView.image = UIImage(named: currentData[index].imageUrl!)
            }
        }
        scrollView.contentOffset = CGPoint(x: viewSize.width, y: 0)
    }
    
    // MARK: Timer
    fileprivate func startTimer() {
        let selector = #selector(nextImage)
        timer = Timer(timeInterval: 3.0, target: self, selector: selector, userInfo: nil, repeats: true)
        RunLoop.main.add(timer!, forMode: RunLoopMode.commonModes)
    }
    
    fileprivate func stopTimer() {
        timer?.invalidate()
        timer = nil
    }
    
    func nextImage() {
        scrollView.setContentOffset(CGPoint(x: 2.0 * viewSize.width, y: 0), animated: true)
    }
    
    func touchImage(tap: UITapGestureRecognizer) {
        if let index = tap.view?.tag {
            delegate?.touchAt!(index: index)
        }
    }
    
    // 初始化 滑动视图
    fileprivate var scrollView: UIScrollView = {
        let object = UIScrollView()
        object.bounces = false
        object.showsVerticalScrollIndicator = false
        object.showsHorizontalScrollIndicator = false
        object.isPagingEnabled = true
        return object
    }()
    
    fileprivate var pageControl: UIPageControl = {
        let object = UIPageControl()
        object.hidesForSinglePage = true
        object.currentPageIndicatorTintColor = UIColor.red
        object.pageIndicatorTintColor = UIColor.gray
        return object
    }()
    
}

extension ImageScrollView: UIScrollViewDelegate {
    
    // 当 scrollView 有偏移量时出发
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        var page: Int = 0
        var minDistance: CGFloat = CGFloat(MAXFLOAT)
        for i in 0 ..< 3 {
            let imageView = scrollView.subviews[i] as! UIImageView
            // 由于当前页有用都是中间页,所以当 偏移量==图片宽度时,就是当前页。
            let distance:CGFloat = abs(imageView.x - scrollView.contentOffset.x)
            if distance < minDistance {
                minDistance = distance
                page = imageView.tag
            }
        }
        pageControl.currentPage = page
    }
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        stopTimer()
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        startTimer()
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        updateScrollView()
    }
    
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        updateScrollView()
    }
    
}

extension UIView {
    /// X值
    open var x: CGFloat {
        return self.frame.origin.x
    }
    /// Y值
    open var y: CGFloat {
        return self.frame.origin.y
    }
    /// 宽度
    open var width: CGFloat {
        return self.frame.size.width
    }
    ///高度
    open var height: CGFloat {
        return self.frame.size.height
    }
    open var size: CGSize {
        return self.frame.size
    }
    open var origin: CGPoint {
        return self.frame.origin
    }
}


调用的 ViewController

import UIKit

class ImageScrollViewController: UIViewController {
    
    var imageScrollView = ImageScrollView(frame: CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: 150))
    var data = [ImageScrollData]()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white
        edgesForExtendedLayout = .init(rawValue: 0)
        self.title = "图片无限滚动"
        self.view.addSubview(imageScrollView)
        for i in 1 ... 6 {
            let item = ImageScrollData(imageUrl: "image_scroll_0\(i).jpg", imageDescribe: nil)
            data.append(item)
        }
        imageScrollView.data = data
    }
    
    

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

extension ImageScrollViewController: ImageScrollViewDelegate {
    func touchAt(index: Int) {
        print(index)
    }
}

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • 转眼间我们都打卡第6天了,组长说了,这周作业是记录一下让你情绪失控的时刻。 我一直在想,我们为什么同样的事情,发生...
    笑莉说阅读 363评论 5 18
  • 不知何时开始,焦虑已经不是一种简简单单的心情。很多人开始发脾气,有些人开始郁闷,有些人开始急躁。但是,焦虑是一种常...
    小简猫阅读 406评论 0 1
  • 首先要做到“知”。有三条建议: 摸底大调查。学生总有同校和同乡,学生入校时,班主任一般会有一个调查小问卷,此时可以...
    侯志强阅读 285评论 0 0
  • 两个月前曹蕾跟我说要弄些“好玩”的事情,然后我看到了她的“玩好市集”。 一个月前,我看到曹蕾在朋友圈说要开始挑战每...
    demi小猫阅读 577评论 0 1