1、简单刷新
苹果默认的刷新控件存在的问题:
1>拉到一定距离自动刷新,即使用户没有松手也会刷新,给用户的体验不好,且浪费用户的流量;
2>调用 beginRefreshing,不显示菊花转圈;
刷新的实现思路:
1>、保证用户拉到足够距离之后,放手,才会刷新;
2>、继承自 UIControl,所有的接口定义和苹果原生基本一致,通过 KVO 监听刷新控件的 contentOffset
创建3个文件:
XZRefreshControl - 负责 刷新 相关的’逻辑’处理
XZRefreshView - 负责 刷新 相关的 UI 显示和动画
XZRefreshView.xib - 跟 XZRefreshView 进行关联,负责页面的搭建
在 XZRefreshControl 中
定义记录属性,记录父视图,父视图应该是scrollView或其子类
private weak var scrollView: UIScrollView?
weak的原因:因为父视图本身会对他强引用,addSubView 由父视图来调用,父视图对刷新控件进行强引用,如果刷新控件再对父视图进行强引用,就会造成循环引用
在 willMove toSuperview 方法中,使用 KVO 监听父视图的 contentOffset,并且计算刷新控件的高度,使用 addSubView 添加刷新控件的时候,会调用当前方法
override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
// 判断父视图的类型
guard let sv = newSuperview as? UIScrollView else {
return
}
// 记录父视图
scrollView = sv
// KVO监听父视图的 contentOffset
scrollView?.addObserver(self, forKeyPath: "contentOffset", options: [], context: nil)
}
注意:使用完 KVO 必须释放,否则会引起程序的崩溃
在 removeFromSuperview 中移除监听
注意:要在调用 super.removeFromSuperview( ) 之前移除,因为调用 super.removeFromSuperview( ) 之后,super 就不存在了
override func removeFromSuperview() {
// superView 还存在
superview?.removeObserver(self, forKeyPath: "contentOffset”)
super.removeFromSuperview()
// superView 不存在
}
添加 KVO 的监听方法,在监听方法中监听父视图的 contentOffset;
判断临界点:
private let XZRefreshOffset: CGFloat = 60
判断用户是否拖动,如果拖动:是否超过临界点 & 是否是 Normal 状态,设置状态为 Pulling;如果没有超过临界点 & 状态是 Pulling,设置状态为 Normal;
如果用户没有拖动:判断状态是否是 Pulling,如果是 Pulling,调用开始刷新方法
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context:UnsafeMutableRawPointer?) {
guard let sv = scrollView else {
return
}
let height = -(sv.contentInset.top + sv.contentOffset.y)
if height < 0 {
return
}
// 可以根据高度设置刷新控件的 frame
self.frame = CGRect(x: 0,
y: -height,
width: sv.bounds.width,
height: height)
// 判断临界点 - 只需要判断一次
if sv.isDragging {
if height > XZRefreshOffset && (refreshView.refreshState == .Normal) {
refreshView.refreshState = .Pulling
}else if height <= XZRefreshOffset && (refreshView.refreshState == .Pulling) {
refreshView.refreshState = .Normal
}
}else {
// 放手刷新 - 判断是否超过临界点
if refreshView.refreshState == .Pulling {
beginRefreshing()
// 发送刷新数据事件
sendActions(for: .valueChanged)
}
}
}
创建枚举值,记录刷新的三种状态:普通状态、超过临界点,开始刷新、超过临界点,用户放手,开始刷新
/// 刷新状态
///
/// - Normal: 普通状态,什么都不做
/// - Pulling: 超过临界点,如果放手,开始刷新
/// - WillRefresh: 用户超过临界点,并且放手
enum XZRefreshState {
case Normal
case Pulling
case WillRefresh
}
在 XZRefreshView 中使用刷新状态设置页面和动画
var refreshState: XZRefreshState = .Normal {
didSet {
switch refreshState {
case .Normal:
// 恢复状态
imgIcon.isHidden = false
indicator.stopAnimating()
labelTip.text = "继续拖拽…"
UIView.animate(withDuration: 0.25, animations: {
self.imgIcon.transform = CGAffineTransform.identity
})
case .Pulling:
labelTip.text = "再向下拖拽…"
UIView.animate(withDuration: 0.25, animations: {
self.imgIcon.transform = CGAffineTransform(rotationAngle: (.pi - 0.001))
})
case .WillRefresh:
labelTip.text = "正在刷新中..."
// 隐藏提示图标
imgIcon.isHidden = true
// 显示菊花
indicator.startAnimating()
}
}
}
实现效果
2、下拉刷新带图片
创建 XZPictureRefreshView.xib 将需要展示的图片的底部跟xib的底部相同,在下拉刷新的时候就可以看到图片了
实现效果
3、模仿美团刷新
创建 XZMTRefreshView.xib 负责页面布局
创建 XZMTRefreshView.swift 负责页面的逻辑
最主要的点是:
1>根据父视图的高度,设置袋鼠的缩小/放大;
2>将图片生成 GIF 动画;
3>设置袋鼠始终保持在地球的底部的中心位置;
根据父视图高度修改袋鼠的大小
/// 父视图高度
override var parentViewHeight: CGFloat {
didSet {
print("父视图高度 \(parentViewHeight)")
if parentViewHeight < kangarooCenterYOffset {
return
}
// 高度: 35 -> 126
// scale: 0.2 -> 1
// 高度差 / 最大高度差
// 35 == 1 -> 0.2
// 126 == 0 -> 1
var scale: CGFloat
if parentViewHeight > 126 {
scale = 1
}else {
scale = 1 - (126 - parentViewHeight) / (126 - 35)
}
imgKangarooView.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
房子动画、袋鼠动画和袋鼠的位置的实现
override func awakeFromNib() {
// 1.房子
let bImage1 = #imageLiteral(resourceName: "icon_building_loading_1")
let bImage2 = #imageLiteral(resourceName: "icon_building_loading_2")
// 房子gif
imgBuildingView.image = UIImage.animatedImage(with: [bImage1,bImage2], duration: 0.5)
// 2.地球
let anim = CABasicAnimation(keyPath: "transform.rotation")
// 2 * Double.pi 顺时针转 -2 * Double.pi 逆时针转
anim.toValue = -2 * Double.pi
anim.repeatCount = MAXFLOAT
anim.duration = 3
anim.isRemovedOnCompletion = false
imgEarthIconView.layer.add(anim, forKey: nil)
// 3.袋鼠:先设置锚点,然后设置 frame 或 center
// 0> 设置袋鼠动画
let kImage1 = #imageLiteral(resourceName: "icon_small_kangaroo_loading_1")
let kImage2 = #imageLiteral(resourceName: "icon_small_kangaroo_loading_2")
imgKangarooView.image = UIImage.animatedImage(with: [kImage1, kImage2], duration: 0.5)
// 1>设置锚点
imgKangarooView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
// 2>设置center
let x = self.bounds.width * 0.5
let y = self.bounds.height - kangarooCenterYOffset
imgKangarooView.center = CGPoint(x: x, y: y)
imgKangarooView.transform = CGAffineTransform(scaleX: 0.2, y: 0.2)
}
实现效果