好久沒寫了更新一個輪播圖模塊,簡單實用
創(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
}
}