Swift图片轮播
- 效果图
- 核心思想
一个UIScrollView,三个UIImageView,一个[UIImage](至少两张图片)
ScrollView的ContentOffset等于三倍屏幕宽,ImageView在ScrollView上的位置始终固定,当滚动时修改各自对应的图片,滚动完成后将Scrollview的ContentOffset置于中间,这样就形成图片循环轮播的”假象”。
左滑示意图:
右滑示意图:
核心代码如下:
private func updateImage() {
if currentPage == 0 {
leftImageView.image = imageArray.last
centerImageView.image = imageArray[currentPage]
rightImageView.image = imageArray[currentPage + 1]
} else if currentPage == imageArray.count - 1 {
leftImageView.image = imageArray[currentPage - 1]
centerImageView.image = imageArray[currentPage]
rightImageView.image = imageArray.first
} else {
leftImageView.image = imageArray[currentPage - 1]
centerImageView.image = imageArray[currentPage]
rightImageView.image = imageArray[currentPage + 1]
}
if let completeOperate = operate {
completeOperate(page: currentPage)
}
pageControl.currentPage = currentPage
scrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false)
}
- 具体实现过程
本文是封装了一个继承于UIView的类,需要的时候直接拿出来用就行了。
属性
private var scrollView = UIScrollView()
private var pageControl = UIPageControl()
private var leftImageView = UIImageView()
private var centerImageView = UIImageView()
private var rightImageView = UIImageView()
private var currentPage = 0
private var width: CGFloat!
private var height: CGFloat!
private var timer: NSTimer?
/// 滚动方向
enum RollingDirection : Int {
case Left
case Right
}
/// 指示器当前页颜色
var currentPageIndicatorTintColor:UIColor = .whiteColor(){
willSet{
pageControl.currentPageIndicatorTintColor = newValue
}
}
/// 指示器颜色
var pageIndicatorTintColor:UIColor = .whiteColor(){
willSet{
pageControl.pageIndicatorTintColor = newValue
}
}
/// 是否自动滚动
var autoRoll = false {
willSet {
if newValue {
startTimer()
} else {
stopTimer()
}
}
}
/// 滚动方向
var direction: RollingDirection = .Right {
willSet {
stopTimer()
}
didSet {
if autoRoll {
startTimer()
}
}
}
/// 间隔时间
var timeInterval: NSTimeInterval = 3 {
willSet {
stopTimer()
}
didSet {
if autoRoll {
startTimer()
}
}
}
/// 图片数组
var imageArray: [UIImage] = [] {
willSet {
stopTimer()
currentPage = 0
pageControl.numberOfPages = newValue.count
}
didSet {
updateImage()
if autoRoll {
startTimer()
}
}
}
/// 滚动完成响应事件
var operate: ((page: Int)->())?
自定义构造函数,使用时可以选择是否自动滚动,滚动时间和方向等
/**
自定义构造函数
- parameter frame: frame
- parameter isAutoRoll: 是否自动滚动
- parameter rollDirection: 滚动方向
- parameter timeInt: 滚动时间间隔
- parameter images: 图片数组
- parameter scrollCompleteOperate: 滚动完成响应事件,参数page为当前页数
*/
init(frame: CGRect,
isAutoRoll: Bool?,
rollDirection: RollingDirection?,
timeInt: NSTimeInterval?,
images:[UIImage],
scrollCompleteOperate:((page: Int)->())?) {
super.init(frame: frame)
initializeUserInterface()
imageArray = images
pageControl.numberOfPages = imageArray.count
if let autoR = isAutoRoll {
autoRoll = autoR
}
if let direct = rollDirection {
direction = direct
}
if let timeI = timeInt {
timeInterval = timeI
}
if let completeOperate = scrollCompleteOperate {
operate = completeOperate
}
//初始化
updateImageData()
startTimer()
}
重写父类构造函数
//重写父类初始化方法
override init(frame: CGRect) {
super.init(frame: frame)
initializeUserInterface()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
定时器相关,这里使用自定义类来创建定时器,下文会提到,防止定时器循环引用导致视图控制器不能被释放
//启动定时器
private func startTimer() {
timer = nil
//调用自定义对象,让timer对其进行强引用,而不对视图控制器强引用
timer = WeakTimerObject.scheduledTimerWithTimeInterval(timeInterval, aTargat: self, aSelector: #selector(pageRoll), userInfo: nil, repeats: true)
}
//关闭定时器
private func stopTimer() {
if let _ = timer?.valid {
timer?.invalidate()
timer = nil
}
}
//定时器触发方法
@objc private func pageRoll() {
switch direction {
case .Left:
scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
case .Right:
scrollView.setContentOffset(CGPoint(x: width * 2, y: 0), animated: true)
}
}
滑动方向判断,无论是手动滑动还是自动滑动都要判断左滑还是右滑
//判断向左滑动还是向右滑动
private func judgeDirection(ratio: CGFloat) {
if ratio < 1 {
if currentPage == 0 {
currentPage = imageArray.count - 1
} else {
currentPage -= 1
}
} else if ratio > 1 {
if currentPage == imageArray.count - 1 {
currentPage = 0
} else {
currentPage += 1
}
}
updateImage()
}
实现ScrollView协议方法
//MARK:-scrollViewDelegate
//手动滑动停止调用
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
judgeDirection(scrollView.contentOffset.x / width)
}
//自动滑动停止调用
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
judgeDirection(scrollView.contentOffset.x / width)
}
- 使用操作
1.调用自定义构造函数
let imageRollView = ImageScrollView(frame: CGRect(x: 0, y: 64, width: SCREEN_WIDTH, height: SCREEN_HEIGHT - 64),
isAutoRoll: true,
rollDirection: .Right,
timeInt: 4,
images: ary) { (page) in
print("第\(page)页")
}
self.view.addSubview(imageRollView)
2.调用默认构造函数
let imageRollView = ImageScrollView(frame: CGRect(x: 0, y: 64, width: SCREEN_WIDTH, height: SCREEN_HEIGHT - 64))
imageRollView.imageArray = ary
imageRollView.autoRoll = true
imageRollView.timeInterval = 3
imageRollView.direction = .Left
imageRollView.operate = {(page) in
print("第\(page)页")
}
self.view.addSubview(imageRollView)
定时器问题
- 问题描述
Demo完成后检查发现问题,在VC中开启定时器,图片轮播正常,并在析构函数中将定时器移除:
deinit {
if let _ = timer?.valid {
timer?.invalidate()
timer = nil
print("停止定时器")
}
}
正常情况下,VC出栈后会自动释放内存并调用deinit()函数,然而问题出现了,VC出栈后并没有调用deinit()函数,这就意味VC并没有释放,定时器一直处于开启状态:
最终发现问题原因:NSTimer对象添加到Runloop的时候,会被Runloop强引用,同时NSTimer对象会对target对象强引用,从而导致循环引用。在一个视图控制器中开启一个定时器,该视图控制器释放前,如果定时器未从Runloop中移除,那么该视图控制器都不会被释放
- 解决方法
解决问题的核心是打破循环引用。本文解决方法是创建一个类,在类中实现定时器scheduledTimerWithTimeInterval()方法,创建定时器的时候就使用该类来创建,目的是让NSTimer的target为该类的对象,而不是视图控制器,这样NSTimer对该类对象进行强引用,而不对视图控制器进行强引用,这样就打破循环引用这个环了。
1.构造一个scheduledTimerWithTimeInterval()方法:
class WeakTimerObject: NSObject {
weak var targat: AnyObject?
var selector: Selector?
var timer: NSTimer?
static func scheduledTimerWithTimeInterval(interval: NSTimeInterval,
aTargat: AnyObject,
aSelector: Selector,
userInfo: AnyObject?,
repeats: Bool) -> NSTimer {
let weakObject = WeakTimerObject()
weakObject.targat = aTargat
weakObject.selector = aSelector
weakObject.timer = NSTimer.scheduledTimerWithTimeInterval(interval,
target: weakObject,
selector: #selector(fire),
userInfo: userInfo,
repeats: repeats)
return weakObject.timer!
}
2.将WeakTimerObject的对象设置为NSTimer的target,并响应定时器方法
func fire(ti: NSTimer) {
if let _ = target {
targat?.performSelector(selector!, withObject: ti.userInfo)
} else {
timer?.invalidate()
}
}
}
- Demo下载
Swift图片无限轮播