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

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

創(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)絡請求回來后吧圖片地址數(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"
        ]

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

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

下面是輪播圖實現(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 {
    
    /// 輪播圖圖片點擊代理
    ///
    /// - Parameters:
    ///   - bannerView: o輪播圖
    ///   - index: 點擊的圖片下標
    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ù)組的最后一位添加傳進來的第一張圖片 1 2 3 4 5 6 1
            self.urlArray.append(urlArray.first!)
            /**
             在數(shù)組的第一位添加傳進來的最后一張圖片 6 1 2 3 4 5 6 1
             insert 插入元素  atIndex: 根據(jù)下標
             */
            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: 遠程圖片數(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: 遠程圖片數(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 () {
        /**
         獲取當前圖片的X位置
         也就是定時器再次出發(fā)時滾動視圖上正在顯示的是哪一張圖片
         */
        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:持續(xù)時間
                 animations:動畫內(nèi)容
                 這個動畫執(zhí)行 0.2秒 后進入下一個方法
                 */
                
                //往最后一張圖片走
                self.scrollView.contentOffset = CGPoint(x: nextX, y: 0);
                
                /**
                 改變對應的分頁控件顯示圓點
                 */
                self.pageView.currentPage = 0;
            }) { (finished) in
                /**
                 動畫效果的第二個方法
                 completion: 回調(diào)方法 (完成\結束的意思)
                 上一個方法結束后進入這個方法
                 */
                
                //往第二張圖片走
                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);
        
        //分頁控件上要顯示的圓點數(shù)量
        pageView.numberOfPages = urlArray.count - 2;
    }
}
// MARK:- 事件
extension PDBannerView{
    fileprivate func imageViewClick(_ index : Int) {
        /// 傳入的下標是遍歷下標, 需要減一 變成外界數(shù)組下標
        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)) {
            /** 設置定時器的觸發(fā)時間, 延后3秒觸發(fā) */
            PDGCDTimer.shared.resumeTimer(timerName: timerName, delay: 3.0)
        }
        
        //獲取當前滾動視圖的偏移量
        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.reibang.com/p/e20a4aca2c3f

感謝大佬分享:http://www.reibang.com/u/c75b18e14ddf

下面是用法

 PDGCDTimer.shared.scheduledDispatchTimer(timerName: timerName, timeInterval: 3.0) {
            DispatchQueue.main.async {
                ///要做的事情,因為項目需要所以GCDTimer的默認是全局并發(fā)隊列.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)

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

//
//  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定時器, 自動開始執(zhí)行的
    ///
    /// - Parameters:
    ///   - name: 定時器名字, 因為是單例類, 所以需要傳入一個不會重復的名字
    ///   - timeInterval: 時間間隔
    ///   - queue: 隊列, 默認是 .global()
    ///   - repeats: 是否重復, 默認 true
    ///   - action: 執(zhí)行任務的閉包
    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()操作后, 是不可以被直接釋放的,
        /// 如果想關閉一個執(zhí)行了suspend()操作的計時器, 需要先執(zhí)行resume(), 再執(zhí)行cancel()
        /// 因為目前沒找到判斷定時器是否是掛起狀態(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
    }
    
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子葫哗,更是在濱河造成了極大的恐慌蜈亩,老刑警劉巖煮盼,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圆凰,死亡現(xiàn)場離奇詭異铸敏,居然都是意外死亡缚忧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門杈笔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闪水,“玉大人,你說我怎么就攤上這事桩撮《氐冢” “怎么了峰弹?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵店量,是天一觀的道長。 經(jīng)常有香客問我鞠呈,道長融师,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任蚁吝,我火速辦了婚禮旱爆,結果婚禮上,老公的妹妹穿的比我還像新娘窘茁。我一直安慰自己怀伦,他們只是感情好,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布山林。 她就那樣靜靜地躺著房待,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上桑孩,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天拜鹤,我揣著相機與錄音,去河邊找鬼流椒。 笑死敏簿,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的宣虾。 我是一名探鬼主播惯裕,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绣硝!你這毒婦竟也來了轻猖?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤域那,失蹤者是張志新(化名)和其女友劉穎咙边,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體次员,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡败许,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了淑蔚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片市殷。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖刹衫,靈堂內(nèi)的尸體忽然破棺而出醋寝,到底是詐尸還是另有隱情,我是刑警寧澤带迟,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布音羞,位于F島的核電站,受9級特大地震影響仓犬,放射性物質(zhì)發(fā)生泄漏嗅绰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一搀继、第九天 我趴在偏房一處隱蔽的房頂上張望窘面。 院中可真熱鬧,春花似錦叽躯、人聲如沸财边。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酣难。三九已至们童,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鲸鹦,已是汗流浹背慧库。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留馋嗜,地道東北人齐板。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像葛菇,于是被迫代替她去往敵國和親甘磨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348