iOS-Swift版的轮播图模块(含GCD的定时器)

好久没写了更新一个轮播图模块,简单实用

创建部分

///轮播图
    private lazy var bannerView : PDBannerView = {
        let bannerView = PDBannerView(
            frame: CGRect(
                x: 0,
                y: 0,
                width: 300,
                height: 200
            )
        )
        bannerView.delegate = self
        return bannerView
    }()

数据源部分,重写了didSet, 等网络请求回来后吧图片地址数组赋值过去就好了

///图片是在网上随便找的
bannerView.urlArray = [
            "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2513543930,426541466&fm=26&gp=0.jpg",
            "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4292350659,3787586302&fm=26&gp=0.jpg",
            "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=129237233,3164604892&fm=26&gp=0.jpg",
            "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1058535659,1441358703&fm=26&gp=0.jpg"
        ]

然后实现代理协议

// MARK:- 轮播图代理
extension ConsultingController : PDBannerViewDelegate {
    func selectImage(bannerView: PDBannerView, index: Int) {
        PDLog("点击了图片\(index)" )
    }
}

下面是轮播图实现类,内部包含一个DispatchSource的封装

//
//  PDBannerView.swift
//  MedicalCare
//
//  Created by 裴铎 on 2019/4/24.
//  Copyright © 2019 裴铎. All rights reserved.
//

import UIKit
import Kingfisher
import RxSwift
import RxCocoa

/// 轮播图代理
protocol PDBannerViewDelegate : NSObjectProtocol {
    
    /// 轮播图图片点击代理
    ///
    /// - Parameters:
    ///   - bannerView: o轮播图
    ///   - index: 点击的图片下标
    func selectImage(bannerView : PDBannerView, index : Int)
}

/// 轮播图
class PDBannerView: UIView {
    /// 代理
    weak var delegate : PDBannerViewDelegate?
    /// 图片数组
    var urlArray : [String] = [String](){
        didSet {
            if urlArray.count <= 1 {
                return
            }
            //在数组的最后一位添加传进来的第一张图片 1 2 3 4 5 6 1
            self.urlArray.append(urlArray.first!)
            /**
             在数组的第一位添加传进来的最后一张图片 6 1 2 3 4 5 6 1
             insert 插入元素  atIndex: 根据下标
             */
            self.urlArray.insert(urlArray.last!, at: 0)
            setSubviews()
        }
    }
    ///定时器名字
    fileprivate (set) var timerName : String = "PDBannerViewTimer"
    /// 垃圾袋
    fileprivate var bag = DisposeBag()
    /// 占位图片 名
    fileprivate var placeholderImageName : String = ""
    /// 宽
    fileprivate var bannerViewWidth : CGFloat = 0
    /// 高
    fileprivate var bannerViewHeight: CGFloat = 0
    ///滚动视图
    fileprivate lazy var scrollView : UIScrollView = {
        let scroll = UIScrollView()
        scroll.frame = CGRect(x: 0, y: 0, width: self.pd_width, height: self.pd_height)
        //滚动式图的代理
        scroll.delegate = self;
        //分页滚动效果 yes
        scroll.isPagingEnabled = true;
        //能否滚动
        scroll.isScrollEnabled = true;
        //弹簧效果 NO
        scroll.bounces = false;
        //垂直滚动条
        scroll.showsVerticalScrollIndicator = false;
        //水平滚动条
        scroll.showsHorizontalScrollIndicator = false;
        return scroll
    }()
    ///分页控件
    fileprivate lazy var pageView : UIPageControl = {
        let page = UIPageControl()
        page.frame = CGRect(x: 0, y: self.pd_height - 20, width: self.pd_width, height: 20)
        //分页控件不允许和用户交互(不许点击)
        page.isUserInteractionEnabled = false;
        //设置 默认点 的颜色
        page.pageIndicatorTintColor = ColorWithHex(hex: "ffffff")
        //设置 滑动点(当前点) 的颜色
        page.currentPageIndicatorTintColor = ColorWithHex(hex: "000000")
        return page
    }()
    
    fileprivate override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    /// 构造器
    ///
    /// - Parameters:
    ///   - frame: 轮播图的加载位置
    ///   - urlArray: 远程图片数组, 不能少于2张图片
    ///   - placeholderImage: 占位图片名
    convenience init(frame: CGRect, placeholderImage : String = "234234") {
        self.init(frame: frame)
        processTheDataSource(frame: frame, placeholderImage: placeholderImage)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
// MARK:- 自定义函数
extension PDBannerView {
    /// 处理数据
    ///
    /// - Parameters:
    ///   - frame: 轮播图的加载位置
    ///   - urlArray: 远程图片数组
    ///   - placeholderImage: 占位图片名
    fileprivate func processTheDataSource(frame: CGRect, placeholderImage : String) {
        bannerViewWidth  = frame.size.width
        bannerViewHeight = frame.size.height
        placeholderImageName = placeholderImage
        
        initUI()
    }
    
    /// GCD定时器
    fileprivate func addGCDTimer() {
        PDGCDTimer.shared.scheduledDispatchTimer(timerName: timerName, timeInterval: 3.0) {
            DispatchQueue.main.async {
                self.setTimerEventHandler()
            }
        }
    }
    
    fileprivate func setTimerEventHandler () {
        /**
         获取当前图片的X位置
         也就是定时器再次出发时滚动视图上正在显示的是哪一张图片
         */
        let currentX : CGFloat = scrollView.contentOffset.x;
            
        /**
         获取下一张图片的X位置
         当前位置 + 一个Banner的宽度
         */
        let nextX : CGFloat = currentX + bannerViewWidth;
            
        /**
         判断滚动视图上将要显示的图片是最后一张时
         通过X值来判断 所以要 self.dataArray.count - 1
         */
        if (nextX == CGFloat(urlArray.count - 1) * bannerViewWidth) {
            
            /**
             UIView的动画效果方法(分两个方法)
             */
            UIView.animate(withDuration: 0.2, animations: {
                /**
                 动画效果的第一个方法
                 Duration:持续时间
                 animations:动画内容
                 这个动画执行 0.2秒 后进入下一个方法
                 */
                
                //往最后一张图片走
                self.scrollView.contentOffset = CGPoint(x: nextX, y: 0);
                
                /**
                 改变对应的分页控件显示圆点
                 */
                self.pageView.currentPage = 0;
            }) { (finished) in
                /**
                 动画效果的第二个方法
                 completion: 回调方法 (完成\结束的意思)
                 上一个方法结束后进入这个方法
                 */
                
                //往第二张图片走
                self.scrollView.contentOffset = CGPoint(x: self.bannerViewWidth, y: 0);
            }
        }else{//如果滚动视图上要显示的图片不是最后一张时
            
            //显示下一张图片
            UIView.animate(withDuration: 0.2, animations: {
                //让下一个图片显示出来
                self.scrollView.contentOffset = CGPoint( x: nextX, y: 0);
                
                //改变对应的分页控件显示圆点
                self.pageView.currentPage = Int(self.scrollView.contentOffset.x / self.bannerViewWidth - 1);
            }) { (finished) in
                //改变对应的分页控件显示圆点
                self.pageView.currentPage = Int(self.scrollView.contentOffset.x / self.bannerViewWidth - 1);
            }
        }
    }
    
    /// 字符串转URL, 并编码
    ///
    /// - Parameter urlString: 字符串
    /// - Returns: URL
    fileprivate func encodingURL(_ urlString : String) -> URL {
        /** 对字符串进行转吗 */
        var charSet = CharacterSet.urlQueryAllowed
        charSet.insert(charactersIn: "#")
        let encodingURLString = urlString.addingPercentEncoding(withAllowedCharacters: charSet ) ?? urlString
        let url : URL = URL(string: encodingURLString)!
        return url
    }
}
// MARK:- UI
extension PDBannerView {
    ///初始化UI
    fileprivate func initUI() {
        //初始化时把scrollView 加载到bannerView上
        addSubview(scrollView)
        //初始化时把分页控件加载到bannerView中
        addSubview(pageView)
    }
    ///添加子视图
    fileprivate func setSubviews() {
        scrollView.pd_removeAllSubviews()
        for (index, url) in urlArray.enumerated() {
            let imageView = UIImageView()
            imageView.image = UIImage(named: placeholderImageName)
            imageView.frame = CGRect(x: CGFloat(index) * bannerViewWidth, y: 0, width: bannerViewWidth, height: bannerViewHeight)
            let imageUrl = encodingURL(url)
            imageView.kf.setImage(with: imageUrl)
            //让图片可以与用户交互
            imageView.isUserInteractionEnabled = true;
            //初始化一个点击手势
            let tap = UITapGestureRecognizer()
            imageView.addGestureRecognizer(tap)
            tap.rx.event.subscribe(onNext: { (_) in
                self.imageViewClick(index)
            }).disposed(by: bag)
            scrollView.addSubview(imageView)
        }
        guard urlArray.count > 1 else {
            return
        }
        //初始化时加载定时器
        addGCDTimer()
        setSuperview()
    }
    /// 设置父视图属性
    fileprivate func setSuperview() {
        /**
         滚动范围(手动拖拽时的范围)
         如果不写就不能手动拖拽(但是定时器可以让图片滚动)
         */
        scrollView.contentSize = CGSize(width: bannerViewWidth * CGFloat(urlArray.count), height: bannerViewHeight)
        //滚动视图的起始偏移量
        scrollView.contentOffset = CGPoint(x: bannerViewWidth, y: 0);
        
        //分页控件上要显示的圆点数量
        pageView.numberOfPages = urlArray.count - 2;
    }
}
// MARK:- 事件
extension PDBannerView{
    fileprivate func imageViewClick(_ index : Int) {
        /// 传入的下标是遍历下标, 需要减一 变成外界数组下标
        let arrayIndex = index - 1
        if delegate != nil {
            delegate?.selectImage(bannerView: self, index: arrayIndex)
        }
    }
}
// MARK:- 滚动代理
extension PDBannerView : UIScrollViewDelegate {
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        PDGCDTimer.shared.suspendTimer(timerName: timerName)
    }
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        pageView.currentPage = Int(scrollView.contentOffset.x / bannerViewWidth - 1);
    }
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        //判断是否有定时器
        if (PDGCDTimer.shared.isExistTimer(timerName: timerName)) {
            /** 设置定时器的触发时间, 延后3秒触发 */
            PDGCDTimer.shared.resumeTimer(timerName: timerName, delay: 3.0)
        }
        
        //获取当前滚动视图的偏移量
        let currentPoint : CGPoint = scrollView.contentOffset;
        
        /** 判断拖拽完成后将要显示的图片时第几张 6 1 2 3 4 5 6 1 */
        //如果是数组内的最后一张图片 1
        if (currentPoint.x == CGFloat(urlArray.count - 1) * bannerViewWidth) {
            
            //改变偏移量 显示数组内的第一张图片 1
            scrollView.contentOffset = CGPoint(x: bannerViewWidth, y: 0);
        }
        
        //如果是数组内的第一张图片 6
        if (currentPoint.x == 0) {
            
            //改变偏移量 显示数组内的 第二个图片6
            scrollView.contentOffset = CGPoint(x: CGFloat(urlArray.count - 2) * bannerViewWidth, y: 0);
        }
        
        /**
         如果是图片数组的第一张图片 或 最后一张图片时
         滚动视图的偏移量发生了改变
         所以之前的偏移量变量不能再使用了 (获取一个新的偏移量)
         */
        //获取新的滚佛那个视图偏移量
        let newPoint : CGPoint = scrollView.contentOffset;
        
        //改变分页控件上的页码
        pageView.currentPage = Int(newPoint.x / bannerViewWidth - 1);
    }
}

下面是一个GCD定时器的封装,原文地址:https://www.jianshu.com/p/e20a4aca2c3f

感谢大佬分享:https://www.jianshu.com/u/c75b18e14ddf

下面是用法

 PDGCDTimer.shared.scheduledDispatchTimer(timerName: timerName, timeInterval: 3.0) {
            DispatchQueue.main.async {
                ///要做的事情,因为项目需要所以GCDTimer的默认是全局并发队列.global(),刷新UI需要回到主
            }
        }

取消某一个定时器

PDGCDTimer.shared.cancleTimer(timerName: timerName)

判断某一个定时器是否存在

if PDGCDTimer.shared.isExistTimer(timerName: timerName) {
            ///定时器存在
        }

暂停某一个定时器

PDGCDTimer.shared.suspendTimer(timerName: timerName)

重新开启某一个定时器

PDGCDTimer.shared.resumeTimer(timerName: timerName)

几秒后重新开启某一个定时器

PDGCDTimer.shared.resumeTimer(timerName: timerName, delay: 3)

下面是实现文件

//
//  PDGCDTimer.swift
//  MedicalCare
//
//  Created by 裴铎 on 2019/4/23.
//  Copyright © 2019 裴铎. All rights reserved.
//

import Foundation

/// 定时器任务闭包
typealias ActionBlock = () -> ()

class PDGCDTimer {
    ///单例
    static let shared = PDGCDTimer()
    
    /// 定时器集合
    lazy var timerContainer = [String: DispatchSourceTimer]()
    
    /// GCD定时器, 自动开始执行的
    ///
    /// - Parameters:
    ///   - name: 定时器名字, 因为是单例类, 所以需要传入一个不会重复的名字
    ///   - timeInterval: 时间间隔
    ///   - queue: 队列, 默认是 .global()
    ///   - repeats: 是否重复, 默认 true
    ///   - action: 执行任务的闭包
    func scheduledDispatchTimer(timerName : String?, timeInterval: Double, queue: DispatchQueue = .global(), repeats: Bool = true, action: @escaping ActionBlock) {
        
        if timerName == nil || timerName == "" {
            fatalError("timerName Can't be empty")
        }
        
        var timer = timerContainer[timerName!]
        if timer == nil {
            timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
            timer?.resume()
            timerContainer[timerName!] = timer
        }
        //精度0.1秒
        timer?.schedule(deadline: .now(), repeating: timeInterval, leeway: DispatchTimeInterval.milliseconds(100))
        timer?.setEventHandler(handler: { [weak self] in
            action()
            if repeats == false {
                self?.cancleTimer(timerName: timerName)
            }
        })
    }
    
    /// 暂停定时器
    ///
    /// - Parameter timerName: 定时器名字
    func suspendTimer(timerName : String?) {
        guard let timer = timerContainer[timerName!] else {
            return
        }
        timer.suspend()
    }
    
    /// 开始定时器
    ///
    /// - Parameter timerName: 定时器名字
    func resumeTimer(timerName : String?) {
        guard let timer = timerContainer[timerName!] else {
            return
        }
        guard timer.isCancelled == false else {
            return
        }
        timer.resume()
    }
    
    /// 延时几秒后开始定时器
    ///
    /// - Parameters:
    ///   - timerName: 定时器名字
    ///   - delay: 几秒后
    func resumeTimer(timerName : String?, delay : Double) {
        guard let timer = timerContainer[timerName!] else {
            return
        }
        guard timer.isCancelled == false else {
            return
        }
        DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
            timer.resume()
        }
    }
    
    /// 取消定时器
    ///
    /// - Parameter name: 定时器名字
    func cancleTimer(timerName : String?) {
        guard let timer = timerContainer[timerName!] else {
            return
        }
        /// gcdTimer执行了suspend()操作后, 是不可以被直接释放的,
        /// 如果想关闭一个执行了suspend()操作的计时器, 需要先执行resume(), 再执行cancel()
        /// 因为目前没找到判断定时器是否是挂起状态的方法, 所以在取消定时器前都执行一次开始操作,
        timer.resume()
        timerContainer.removeValue(forKey: timerName!)
        timer.cancel()
    }
    
    
    /// 检查定时器是否已存在
    ///
    /// - Parameter name: 定时器名字
    /// - Returns: 是否已经存在定时器
    func isExistTimer(timerName : String?) -> Bool {
        return timerContainer[timerName!] == nil ? false : true
    }
    
}

你可能感兴趣的:(iOS-Swift版的轮播图模块(含GCD的定时器))