iOS-Swift版的輪播圖模塊(含GCD的定時器)

好久沒寫了更新一個輪播圖模塊,簡單實(shí)用

創(chuàng)建部分

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

數(shù)據(jù)源部分,重寫了didSet, 等網(wǎng)絡(luò)請求回來后吧圖片地址數(shù)組賦值過去就好了

///圖片是在網(wǎng)上隨便找的
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"
        ]

然后實(shí)現(xiàn)代理協(xié)議

// MARK:- 輪播圖代理
extension ConsultingController : PDBannerViewDelegate {
    func selectImage(bannerView: PDBannerView, index: Int) {
        PDLog("點(diǎn)擊了圖片\(index)" )
    }
}

下面是輪播圖實(shí)現(xiàn)類,內(nèi)部包含一個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 {
    
    /// 輪播圖圖片點(diǎn)擊代理
    ///
    /// - Parameters:
    ///   - bannerView: o輪播圖
    ///   - index: 點(diǎn)擊的圖片下標(biāo)
    func selectImage(bannerView : PDBannerView, index : Int)
}

/// 輪播圖
class PDBannerView: UIView {
    /// 代理
    weak var delegate : PDBannerViewDelegate?
    /// 圖片數(shù)組
    var urlArray : [String] = [String](){
        didSet {
            if urlArray.count <= 1 {
                return
            }
            //在數(shù)組的最后一位添加傳進(jìn)來的第一張圖片 1 2 3 4 5 6 1
            self.urlArray.append(urlArray.first!)
            /**
             在數(shù)組的第一位添加傳進(jìn)來的最后一張圖片 6 1 2 3 4 5 6 1
             insert 插入元素  atIndex: 根據(jù)下標(biāo)
             */
            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)
        //分頁控件不允許和用戶交互(不許點(diǎn)擊)
        page.isUserInteractionEnabled = false;
        //設(shè)置 默認(rèn)點(diǎn) 的顏色
        page.pageIndicatorTintColor = ColorWithHex(hex: "ffffff")
        //設(shè)置 滑動點(diǎn)(當(dāng)前點(diǎn)) 的顏色
        page.currentPageIndicatorTintColor = ColorWithHex(hex: "000000")
        return page
    }()
    
    fileprivate override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    /// 構(gòu)造器
    ///
    /// - Parameters:
    ///   - frame: 輪播圖的加載位置
    ///   - urlArray: 遠(yuǎn)程圖片數(shù)組, 不能少于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:- 自定義函數(shù)
extension PDBannerView {
    /// 處理數(shù)據(jù)
    ///
    /// - Parameters:
    ///   - frame: 輪播圖的加載位置
    ///   - urlArray: 遠(yuǎn)程圖片數(shù)組
    ///   - 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 () {
        /**
         獲取當(dāng)前圖片的X位置
         也就是定時器再次出發(fā)時滾動視圖上正在顯示的是哪一張圖片
         */
        let currentX : CGFloat = scrollView.contentOffset.x;
            
        /**
         獲取下一張圖片的X位置
         當(dāng)前位置 + 一個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:持續(xù)時間
                 animations:動畫內(nèi)容
                 這個動畫執(zhí)行 0.2秒 后進(jìn)入下一個方法
                 */
                
                //往最后一張圖片走
                self.scrollView.contentOffset = CGPoint(x: nextX, y: 0);
                
                /**
                 改變對應(yīng)的分頁控件顯示圓點(diǎn)
                 */
                self.pageView.currentPage = 0;
            }) { (finished) in
                /**
                 動畫效果的第二個方法
                 completion: 回調(diào)方法 (完成\結(jié)束的意思)
                 上一個方法結(jié)束后進(jìn)入這個方法
                 */
                
                //往第二張圖片走
                self.scrollView.contentOffset = CGPoint(x: self.bannerViewWidth, y: 0);
            }
        }else{//如果滾動視圖上要顯示的圖片不是最后一張時
            
            //顯示下一張圖片
            UIView.animate(withDuration: 0.2, animations: {
                //讓下一個圖片顯示出來
                self.scrollView.contentOffset = CGPoint( x: nextX, y: 0);
                
                //改變對應(yīng)的分頁控件顯示圓點(diǎn)
                self.pageView.currentPage = Int(self.scrollView.contentOffset.x / self.bannerViewWidth - 1);
            }) { (finished) in
                //改變對應(yīng)的分頁控件顯示圓點(diǎn)
                self.pageView.currentPage = Int(self.scrollView.contentOffset.x / self.bannerViewWidth - 1);
            }
        }
    }
    
    /// 字符串轉(zhuǎn)URL, 并編碼
    ///
    /// - Parameter urlString: 字符串
    /// - Returns: URL
    fileprivate func encodingURL(_ urlString : String) -> URL {
        /** 對字符串進(jìn)行轉(zhuǎn)嗎 */
        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;
            //初始化一個點(diǎn)擊手勢
            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()
    }
    /// 設(shè)置父視圖屬性
    fileprivate func setSuperview() {
        /**
         滾動范圍(手動拖拽時的范圍)
         如果不寫就不能手動拖拽(但是定時器可以讓圖片滾動)
         */
        scrollView.contentSize = CGSize(width: bannerViewWidth * CGFloat(urlArray.count), height: bannerViewHeight)
        //滾動視圖的起始偏移量
        scrollView.contentOffset = CGPoint(x: bannerViewWidth, y: 0);
        
        //分頁控件上要顯示的圓點(diǎn)數(shù)量
        pageView.numberOfPages = urlArray.count - 2;
    }
}
// MARK:- 事件
extension PDBannerView{
    fileprivate func imageViewClick(_ index : Int) {
        /// 傳入的下標(biāo)是遍歷下標(biāo), 需要減一 變成外界數(shù)組下標(biāo)
        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)) {
            /** 設(shè)置定時器的觸發(fā)時間, 延后3秒觸發(fā) */
            PDGCDTimer.shared.resumeTimer(timerName: timerName, delay: 3.0)
        }
        
        //獲取當(dāng)前滾動視圖的偏移量
        let currentPoint : CGPoint = scrollView.contentOffset;
        
        /** 判斷拖拽完成后將要顯示的圖片時第幾張 6 1 2 3 4 5 6 1 */
        //如果是數(shù)組內(nèi)的最后一張圖片 1
        if (currentPoint.x == CGFloat(urlArray.count - 1) * bannerViewWidth) {
            
            //改變偏移量 顯示數(shù)組內(nèi)的第一張圖片 1
            scrollView.contentOffset = CGPoint(x: bannerViewWidth, y: 0);
        }
        
        //如果是數(shù)組內(nèi)的第一張圖片 6
        if (currentPoint.x == 0) {
            
            //改變偏移量 顯示數(shù)組內(nèi)的 第二個圖片6
            scrollView.contentOffset = CGPoint(x: CGFloat(urlArray.count - 2) * bannerViewWidth, y: 0);
        }
        
        /**
         如果是圖片數(shù)組的第一張圖片 或 最后一張圖片時
         滾動視圖的偏移量發(fā)生了改變
         所以之前的偏移量變量不能再使用了 (獲取一個新的偏移量)
         */
        //獲取新的滾佛那個視圖偏移量
        let newPoint : CGPoint = scrollView.contentOffset;
        
        //改變分頁控件上的頁碼
        pageView.currentPage = Int(newPoint.x / bannerViewWidth - 1);
    }
}

下面是一個GCD定時器的封裝,原文地址:http://www.itdecent.cn/p/e20a4aca2c3f

感謝大佬分享:http://www.itdecent.cn/u/c75b18e14ddf

下面是用法

 PDGCDTimer.shared.scheduledDispatchTimer(timerName: timerName, timeInterval: 3.0) {
            DispatchQueue.main.async {
                ///要做的事情,因?yàn)轫?xiàng)目需要所以GCDTimer的默認(rèn)是全局并發(fā)隊(duì)列.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)

下面是實(shí)現(xiàn)文件

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

import Foundation

/// 定時器任務(wù)閉包
typealias ActionBlock = () -> ()

class PDGCDTimer {
    ///單例
    static let shared = PDGCDTimer()
    
    /// 定時器集合
    lazy var timerContainer = [String: DispatchSourceTimer]()
    
    /// GCD定時器, 自動開始執(zhí)行的
    ///
    /// - Parameters:
    ///   - name: 定時器名字, 因?yàn)槭菃卫? 所以需要傳入一個不會重復(fù)的名字
    ///   - timeInterval: 時間間隔
    ///   - queue: 隊(duì)列, 默認(rèn)是 .global()
    ///   - repeats: 是否重復(fù), 默認(rèn) true
    ///   - action: 執(zhí)行任務(wù)的閉包
    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執(zhí)行了suspend()操作后, 是不可以被直接釋放的,
        /// 如果想關(guān)閉一個執(zhí)行了suspend()操作的計時器, 需要先執(zhí)行resume(), 再執(zhí)行cancel()
        /// 因?yàn)槟壳皼]找到判斷定時器是否是掛起狀態(tài)的方法, 所以在取消定時器前都執(zhí)行一次開始操作,
        timer.resume()
        timerContainer.removeValue(forKey: timerName!)
        timer.cancel()
    }
    
    
    /// 檢查定時器是否已存在
    ///
    /// - Parameter name: 定時器名字
    /// - Returns: 是否已經(jīng)存在定時器
    func isExistTimer(timerName : String?) -> Bool {
        return timerContainer[timerName!] == nil ? false : true
    }
    
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容