自带的tableView刷新控件实在太丑了 , 想自己重新做一个自定义的
首先定义一些我们需要的常量
public struct QTableViewRefreshConfig {
struct KeyPaths {
static let ContentOffset = "contentOffset"
static let ContentInset = "contentInset"
static let Frame = "frame"
static let PanGestureRecognizerState = "panGestureRecognizer.state"
}
public static var TriggerOffsetToPull: CGFloat = 95.0
public static var LoadingOffset: CGFloat = 50.0
}
然后是整个控件的刷新状态定义
public
enum QPullDownRefreshState: Int {
case Stopped //停止
case Dragging //拉动
case Bounce //触发回弹
case Loading //刷新中
case BackToStopped //刷新返回
func isAnyOf(values: [QPullDownRefreshState]) -> Bool {
return values.contains({ $0 == self })
}
}
然后 扩展UIsrollView用动态绑定将我们的自定义headerView绑定到UIscrollView中
public extension UIScrollView {
private struct Q_associatedKeys {
static var pullDownRefreshView = "pullToRefreshView"
}
private var pullDownRefreshView: QPullDownRefreshView? {
get {
return objc_getAssociatedObject(self, &Q_associatedKeys.pullDownRefreshView ) as? QPullDownRefreshView
}
set {
objc_setAssociatedObject(self, &Q_associatedKeys.pullDownRefreshView, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
public func Q_addPullDownRefreshWithActionHandler(actionHandler: () -> Void) {
multipleTouchEnabled = false
panGestureRecognizer.maximumNumberOfTouches = 1
let pullDownRefreshView = QPullDownRefreshView()
self.pullDownRefreshView = pullDownRefreshView
pullDownRefreshView.actionHandler = actionHandler
addSubview(pullDownRefreshView)
pullDownRefreshView.observing = true
pullDownRefreshView.backgroundColor = UIColor.redColor()
}
public func Q_removePullToRefresh() {
pullDownRefreshView?.observing = false
pullDownRefreshView?.removeFromSuperview()
}
public func Q_completeLoading() {
}
然后开始写主要的headerView类
public class QPullDownRefreshView: UIView {
}
整个QPullDownRefreshView靠监听UIscroll来驱动的,重新写了NSboject的KVO方法 参考http://www.jianshu.com/p/2e36b451ab43
private var scrollView:UIScrollView!{
get{
return superview as! UIScrollView
}
}
private var originalContentInsetTop: CGFloat = 0.0 { didSet { layoutSubviews() } }
var observing: Bool = false {
didSet {
guard let scrollView = self.scrollView else { return }
if observing {
scrollView.safe_addObserver(self, forKeyPath: QTableViewRefreshConfig.KeyPaths.ContentOffset)
scrollView.safe_addObserver(self, forKeyPath: QTableViewRefreshConfig.KeyPaths.ContentInset)
scrollView.safe_addObserver(self, forKeyPath: QTableViewRefreshConfig.KeyPaths.Frame)
scrollView.safe_addObserver(self, forKeyPath: QTableViewRefreshConfig.KeyPaths.PanGestureRecognizerState)
} else {
scrollView.safe_removeObserver(self, forKeyPath: QTableViewRefreshConfig.KeyPaths.ContentOffset)
scrollView.safe_removeObserver(self, forKeyPath: QTableViewRefreshConfig.KeyPaths.ContentInset)
scrollView.safe_removeObserver(self, forKeyPath: QTableViewRefreshConfig.KeyPaths.Frame)
scrollView.safe_removeObserver(self, forKeyPath: QTableViewRefreshConfig.KeyPaths.PanGestureRecognizerState)
}
}
}
我采用了很多guard语句具体可以参考http://www.jianshu.com/p/3a8e45af7fdd
接着处理监听值的改变,
override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == QTableViewRefreshConfig.KeyPaths.ContentOffset {
if let scrollView = self.scrollView {
if state == .Stopped && scrollView.dragging {
state = .Dragging
}
}
updateLoadingView()
} else if keyPath == QTableViewRefreshConfig.KeyPaths.ContentInset {
if let newContentInsetTop = change?[NSKeyValueChangeNewKey]?.UIEdgeInsetsValue().top {
originalContentInsetTop = newContentInsetTop
}
} else if keyPath == QTableViewRefreshConfig.KeyPaths.Frame {
updateLoadingView()
} else if keyPath == QTableViewRefreshConfig.KeyPaths.PanGestureRecognizerState {
//触发刷新or恢复默认
if let gestureState = scrollView?.panGestureRecognizer.state where gestureState.isAnyOf([.Ended, .Cancelled, .Failed]) {
if state == .Dragging {
if self.actualContentOffsetY >= QTableViewRefreshConfig.TriggerOffsetToPull {
state = .Bounce
} else {
state = .Stopped
}
}
}
}
}
这里用到了一些计算量,无外乎是ScrollContent,SCrollInsetTop的组合计算,可以参考手册
private var currentHeight:CGFloat{
get{
guard let scrollView = self.scrollView else { return 0.0 }
return max(-originalContentInsetTop - scrollView.contentOffset.y, 0)
}
}
private var actualContentOffsetY:CGFloat{
get{
guard let scrollView = self.scrollView else { return 0.0 }
return max(-scrollView.contentInset.top - scrollView.contentOffset.y, 0)
}
}
private var originalContentInsetTop: CGFloat = 0.0 { didSet { layoutSubviews() } }
接着我们写状态state的didset方法,整个刷新的时候,当触发刷新之后UIscroll的scrollContentY就为0了,是靠着ScrollInsectTop的变化来做效果的。
private(set) var state: QPullDownRefreshState = .Stopped{
didSet{
switch state {
case .Stopped:
self.observing = true
self.loadingSharpLayer?.loadingLayerState = .Stopped
case .Dragging: self.loadingSharpLayer?.loadingLayerState = .Dragging
case .Bounce:
if oldValue == .Dragging {
self.loadingSharpLayer?.loadingLayerState = .Bounce
self.observing = false
updateScrollViewContentInset(self.currentHeight, animated: false, completion: { [unowned self]() -> () in
self.state = .Loading
})
}
case .Loading:
updateScrollViewContentInset(QTableViewRefreshConfig.TriggerOffsetToPull, animated: true, completion: { [unowned self]() -> () in
self.loadingSharpLayer?.loadingLayerState = .Loading
self.actionHandler()
})
case .Complete: self.loadingSharpLayer?.loadingLayerState = .Complete
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {[unowned self]() -> () in self.state = .BackToStopped
})
case .BackToStopped:
updateScrollViewContentInset(0, animated: true, completion: { [unowned self]() -> () in
self.state = .Stopped
})
}
}
}
private func updateScrollViewContentInset(contentInsetTop:CGFloat , animated: Bool, completion: (() -> ())?) {
var contentInset = self.scrollView.contentInset
contentInset.top = originalContentInsetTop
contentInset.top += contentInsetTop
let animationBlock = { self.scrollView.contentInset = contentInset }
if animated {
UIView.animateWithDuration(0.4, animations: animationBlock, completion: { _ in
completion!()
})
} else {
animationBlock()
completion!()
}
}
self.loadingSharpLayer?这是一个用来做展示的CAsharplayer的子类
public
enum QloadingViewState: Int {
case Stopped
case Dragging
case Bounce
case Loading
case Complete
}
class LoadingSharpLayer: CAShapeLayer {
var loadingLayerState:QloadingViewState = .Stopped{
didSet{
switch loadingLayerState {
case .Stopped: self.hidden = true
case .Dragging:
if oldValue == .Stopped {
self.hidden = false
self.opacity = 0
let center:CGPoint = CGPointMake(25, 25)
let uPath:UIBezierPath = UIBezierPath(arcCenter: center, radius: 15 , startAngle: 0 , endAngle: 5.5 , clockwise: true)
self.path = uPath.CGPath
self.fillColor = UIColor.clearColor().CGColor
self.strokeColor = UIColor(red: 135.0/256, green: 206.0/256, blue: 250.0/256, alpha: 1).CGColor
self.lineWidth = 5
self.strokeStart = 0
self.strokeEnd = 0
}
case .Loading:
if oldValue == .Bounce {
self.loadingAnimationSwitch = true
}
case .Bounce:
if oldValue == .Dragging {
self.transform = CATransform3DMakeRotation( 0 , 0 , 0 , 1 )
}
case .Complete:
if oldValue == .Loading {
self.loadingAnimationSwitch = false
let uPath:UIBezierPath = UIBezierPath()
uPath.moveToPoint(CGPointMake(7.5, 28))
uPath.addLineToPoint(CGPointMake(20,42.5))
uPath.addLineToPoint(CGPointMake(42.5, 18))
self.path = uPath.CGPath
self.fillColor = UIColor.clearColor().CGColor
self.strokeColor = UIColor(red: 135.0/256, green: 206.0/256, blue: 250.0/256, alpha: 1).CGColor
self.lineWidth = 6
self.strokeStart = 0
self.strokeEnd = 1
}
}
}
}
var loadingAnimationSwitch:Bool = false{
didSet{
if self.loadingAnimationSwitch {
let loadingAnimation:CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
loadingAnimation.fromValue = 0
loadingAnimation.toValue = 2 * M_PI
loadingAnimation.duration = 1
loadingAnimation.repeatCount = 100
loadingAnimation.speed = 1
self.addAnimation(loadingAnimation, forKey: "loadingTransform")
}else{
self.removeAnimationForKey("loadingTransform")
}
}
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(layer: AnyObject) {
super.init(layer: layer)
}
func updateLoadingView(process:CGFloat){
self.strokeEnd = process < 1 ? process : 1
self.opacity = Float(process)
self.transform = CATransform3DMakeRotation( CGFloat(M_PI) * 2 * process , 0, 0, 1)
}
}
当我们dragging的时候,更新loadingView
public func updateLoadingView() {
if let scrollView = self.scrollView{
let width = scrollView.bounds.width
let heightY = self.currentHeight
let height = heightY > QTableViewRefreshConfig.TriggerOffsetToPull ? QTableViewRefreshConfig.TriggerOffsetToPull : heightY
self.frame = CGRect(x: 0.0, y: -height, width: width, height: height)
if let loadingLayer = self.loadingSharpLayer{
CATransaction.begin();
CATransaction.setDisableActions(true)
loadingLayer.position = CGPointMake(width/2, height/2)
loadingLayer.updateLoadingView(self.currentHeight/QTableViewRefreshConfig.TriggerOffsetToPull)
CATransaction.commit();
}
}
self.setNeedsLayout()
}
调用代码
self.tableView!.Q_addPullDownRefreshWithActionHandler {
[unowned self] () -> Void in
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(3 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
self.tableView!.reloadData()
self.tableView!.Q_completeLoading()
})
}
附上效果图