好久没写了更新一个轮播图模块,简单实用
创建部分
///轮播图
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
}
}