自己动手写一个tableView刷新控件

自带的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()
            })
        }

附上效果图

2016-05-05 16_30_31.gif

git地址 https://github.com/lvjiaqijiaqi/tableViewRefresh

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容