1.开发环境
-
xcode 8.3.2
、swift 3.1
-
pod
: Kingfisher-swift版SDWebImage、SnapKit-swift版Masonry,语法更简单
2.原理
基于collectionView实现循环滚动,根据需要展示数据的个数设置较大的组数,并且默认滚动到中间组,然后实现左右滚动
利用Timer实现collectionView的自动滚动,
通过scrollView.contentOffset计算页码,
利用scrollViewWillBeginDragging和scrollViewDidEndDragging处理定时器操作
3.实现步骤
1.初始化view
// 设置布局样式
fileprivate lazy var layout : UICollectionViewFlowLayout = {
var tempLayout : UICollectionViewFlowLayout = UICollectionViewFlowLayout()
tempLayout.minimumLineSpacing = 0;
tempLayout.scrollDirection = .horizontal
return tempLayout
}()
// 初始化collectionView
fileprivate lazy var collectionView : UICollectionView = {
var tempCollectionView : UICollectionView = UICollectionView(frame: self.frame, collectionViewLayout: self.layout)
tempCollectionView.backgroundColor = UIColor.clear
tempCollectionView.isPagingEnabled = true
tempCollectionView.showsVerticalScrollIndicator = false
tempCollectionView.showsHorizontalScrollIndicator = false
tempCollectionView.scrollsToTop = false
tempCollectionView.dataSource = self
tempCollectionView.delegate = self
tempCollectionView.register(IBannerCell.self, forCellWithReuseIdentifier: BannerCellIdentifier)
return tempCollectionView
}()
lazy var bottomView : UIView = {
var tempView = UIView()
tempView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
return tempView
}()
// 初始化PageControl
fileprivate lazy var pageControl : UIPageControl = {
var tempControl = UIPageControl()
tempControl.contentHorizontalAlignment = .right
return tempControl
}()
// 初始化定时器
fileprivate var timer : Timer?
// 初始化数据
fileprivate var imageDatas : Array = []
// 总共有多少组item
fileprivate var totalItemsCount : Int = 0
-
lazy
:是swift
懒加载关键字声明,定义lazy
系统不会创建新的对象,使用lazy
关键字后面只能用var
,不能用let
声明
懒加载其他写法:
// 声明懒加载view
private lazy var bgView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
// 对view进行操作
bgView.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
bgView.layer.cornerRadius = 4
bgView.clipsToBounds = true
··· ···
}
-
fileprivate
:声明属性当前文件私有,便于当前文件extension class
内部访问,如果当前文件不需要使用extension class
,可以定义private
,则当前类私有,extension class
内部将不能访问private
定义的成员变量
2.代理或者闭包数据回调
// MARK: 设置回调
// 闭包
open var selectItem:((Array,Int)->())?
// 代理
weak open var bannerDelegate: IBannerViewDelegate?
-
//MARK : - 注释内容
是swift
的分组注释和oc
的#pragma mark - 注释内容
类似 -
open
:open
是弥补public
语义上的不足,声明其他class
类可以访问 -
swift
的闭包和oc
的block
类似,格式:(参数列表) -> (返回值类型)
2.1代理声明
public protocol IBannerViewDelegate : NSObjectProtocol{
/// 选中某一行banner
///
/// - Parameters:
/// - datas: 选中的数据源
/// - indexItem: 选中的索引
func selectBannerItem(datas : Array ,indexItem : Int) -> Void
}
-
xocde8
快速注释快捷键:option + command + /
3.声明传递数据方法和加载UI
// MARK: 填充数据,开启滚动
open func initDatas(datas : Array ) {
// 设置pageControl最大个数
pageControl.numberOfPages = datas.isEmpty ? 0 : datas.count
imageDatas = datas
// 设置较大的组数,只有1个数据的时候,不左右滑动
totalItemsCount = datas.count == 1 ? 1 : imageDatas.count * 60
collectionView.reloadData()
// 默认显示最中间的那组
collectionView.scrollToItem(at: IndexPath.init(item: 0, section: totalItemsCount / 2), at: .left, animated: false)
weak var weakSelf = self
// 开启定时器,延迟0.05秒。避免runloop阻塞无法启动定时器
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()
+ 0.05) {
weakSelf?.startTimer()
}
initTitleLabel(item: 0)
}
// 初始方法
override init(frame: CGRect) {
super.init(frame: frame)
// 配置UI
configureUI()
}
fileprivate func configureUI() {
self.addSubview(collectionView);
bottomView.addSubview(pageControl)
}
override func layoutSubviews() {
super.layoutSubviews()
collectionView.snp.makeConstraints { (make) in
// make.left.right.bottom.top.equalTo(0)
make.edges.equalToSuperview()
}
bottomView.snp.makeConstraints { (make) in
make.left.right.bottom.equalTo(0)
make.height.equalTo(30)
}
pageControl.snp.makeConstraints { (make) in
make.right.equalTo(-10)
make.top.bottom.equalTo(0)
make.width.equalTo(100)
}
layout.itemSize = CGSize(width: self.frame.size.width, height: self.frame.size.height )
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
isEmpty
: swift空判断 - 重写
init
方法必须写required init?
-
swift
调用方法直接方法名()
- 闭包内部调用成员变量或者方法需要
self.
4.处理collectionView协议
func numberOfSections(in collectionView: UICollectionView) -> Int {
return totalItemsCount;
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageDatas.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BannerCellIdentifier, for: indexPath) as! IBannerCell
cell.setDatas(cellData: imageDatas[indexPath.item])
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 代理回调
if bannerDelegate != nil {
bannerDelegate!.selectBannerItem(datas: imageDatas, indexItem: indexPath.item)
}
// 闭包回调
if selectItem != nil {
selectItem!(imageDatas,indexPath.item)
}
}
5.页码计算
// 开始拖拽停止定时器
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
removeTimer()
}
// 结束拖拽开启定时器
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
startTimer()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if imageDatas.isEmpty || imageDatas.count == 1 {
return
}
// 拖拽计算页码
let page = (Int)(scrollView.contentOffset.x / scrollView.bounds.size.width + 0.5) % imageDatas.count;
self.pageControl.currentPage = page
initTitleLabel(item: page)
}
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview != nil {
removeTimer()
}
}
// MARK: 开启定时器
open func startTimer() {
if timer != nil{
timer!.invalidate()
}
if self.imageDatas.count > 1 {
timer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(nextPage), userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: .commonModes)
}
}
// MARK: 移除定时器
open func removeTimer() {
if timer != nil {
timer!.invalidate()
timer = nil
}
}
// MARK: 下一页
@objc fileprivate func nextPage() {
// 马上显示回最中间那组的数据
let currentIndexPathReset = resetIndexPath()
// 计算出下一个需要展示的位置
var nextItem = currentIndexPathReset.item + 1;
var nextSection = currentIndexPathReset.section
if nextItem == self.imageDatas.count {
nextItem = 0
nextSection += 1
}
let nextIndexPath = IndexPath.init(item: nextItem, section: nextSection)
// 通过动画滚动到下一个位置
self.collectionView.scrollToItem(at: nextIndexPath, at: .left, animated: true)
}
fileprivate func resetIndexPath() -> IndexPath {
// 当前正在展示的位置
let currentIndexPath = self.collectionView.indexPathsForVisibleItems.last
let currentIndexPathReset = IndexPath.init(item: (currentIndexPath?.item)!, section: self.totalItemsCount / 2)
// 马上显示回最中间那组的数据
self.collectionView.scrollToItem(at: currentIndexPathReset, at: .left, animated: false)
return currentIndexPathReset
}
-
@objc
: 保留OC某些特性,#selector(nextPage)
就是使用OC的方式调用方法 - 如果不想使用
@obj
,swift
版的参考UIScreen.main.responds(to: #selector(getter: UIScreen.main.currentMode))
写法
6.cell数据处理
open func setDatas(_ cellData: Any) {
if cellData is IBannerModel {
let model = cellData as! IBannerModel
let url = URL(string: model.url!)!
let placeholderImg = UIImage(named: model.placeholderImage!)!
imageView.kf.setImage(with: url, placeholder: placeholderImg, options: nil, progressBlock: nil, completionHandler: nil)
}
}
-
cellData is IBannerModel
等价于(cellData as AnyObject).isKind(of: IBannerModel.self)
, 这里可以理解为cellData
是否是```IBannerModel``类型 -
cellData as! IBannerModel
: 数据的强转,声明cellData
必须是IBannerModel
类型 -
Kingfisher
的另一方法:imageView.kf.setImage(with: url)