iOS中的下拉刷新SVPullToRefresh

孙嘉
2023-12-01

下拉刷新是一种利用手势刷新用户界面的功能,虽然已经被Twitter申请为专利,但依然不能阻止广大的App开发者在自己的应用中加入该功能。苹果公司甚至在iOS6的sdk中加入了UIRefreshControl,从而实现了系统级的下拉刷新。但是UIRefreshControl是绑定在UITableViewController上的,所以灵活性不高。

如果在网上搜下拉刷新的实现,讨论最多的恐怕是EGOTableViewPullRefresh了,这是一个比较有历史的开源库了,github上最近一次提交都是两年前的事情了。EGOTableViewPullRefresh虽然很好的实现了下拉刷新功能,但是一些OC的新特性比如arc、block没有得到支持,导致加个下拉刷新要写不少代码。

于是乎EGOTableViewPullRefresh的替代者出来了,就是SVPullToRefresh。支持arc,支持block,而且简洁到只需一行就能实现下拉刷新和上拉加载。

SVPullToRefresh的下拉刷新用法相当简单:

1、将下拉刷新控件放在顶部

[csharp] view plain copy
  1. [tableView addPullToRefreshWithActionHandler:^{  
  2.     // prepend data to dataSource, insert cells at top of table view  
  3.     // call [tableView.pullToRefreshView stopAnimating] when done  
  4. }];  

2、将下拉刷新控件放在底部
[csharp] view plain copy
  1. [tableView addPullToRefreshWithActionHandler:^{  
  2.     // prepend data to dataSource, insert cells at top of table view  
  3.     // call [tableView.pullToRefreshView stopAnimating] when done  
  4. } position:SVPullToRefreshPositionBottom];  

3、程序自动调用下拉刷新

[csharp] view plain copy
  1. [tableView triggerPullToRefresh];  

4、临时性禁用下拉刷新
[csharp] view plain copy
  1. tableView.showsPullToRefresh = NO;  
SVPullToRefresh的UI支持自定义

下拉刷新对应的view名叫pullToRefreshView,有如下属性和方法修改它的显示。

[csharp] view plain copy
  1. @property (nonatomic, strong) UIColor *arrowColor;  
  2. @property (nonatomic, strong) UIColor *textColor;  
  3. @property (nonatomic, readwrite) UIActivityIndicatorViewStyle activityIndicatorViewStyle;  
  4.   
  5. - (void)setTitle:(NSString *)title forState:(SVPullToRefreshState)state;  
  6. - (void)setSubtitle:(NSString *)subtitle forState:(SVPullToRefreshState)state;  
  7. - (void)setCustomView:(UIView *)view forState:(SVPullToRefreshState)state;  
简单用法,比如下面一行代码就修改了下拉箭头的颜色。
[csharp] view plain copy
  1. tableView.pullToRefreshView.arrowColor = [UIColor whiteColor];  

上面介绍了下拉刷新的基本用法,下拉加载的用法也差不多,就不再介绍了,可以参考官方资料https://github.com/samvermette/SVPullToRefresh

-------------------------------------------------------------------------------------------------------------------------------------------------------

下面探讨一下SVPullToRefresh背后的原理,SVPullToRefresh能把接口设计的如此简单,肯定有一些过人之处,我们以后在设计控件的时候或许也能从中得到一些启发。

SVPullToRefresh的库其实也就两个类,分别对应下拉刷新和上拉加载,因为两者原理差不多,所以只需要看下拉刷新的实现即可。

[csharp] view plain copy
  1. @interface UIScrollView (SVPullToRefresh)  
  2.   
  3. typedef NS_ENUM(NSUInteger, SVPullToRefreshPosition) {  
  4.     SVPullToRefreshPositionTop = 0,  
  5.     SVPullToRefreshPositionBottom,  
  6. };  
  7.   
  8. - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler;  
  9. - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position;  
  10. - (void)triggerPullToRefresh;  
  11.   
  12. @property (nonatomic, strong, readonly) SVPullToRefreshView *pullToRefreshView;  
  13. @property (nonatomic, assign) BOOL showsPullToRefresh;  
  14.   
  15. @end  

[csharp] view plain copy
  1. @interface SVPullToRefreshView : UIView  
  2.   
  3. @property (nonatomic, strong) UIColor *arrowColor;  
  4. @property (nonatomic, strong) UIColor *textColor;  
  5. @property (nonatomic, strong, readonly) UILabel *titleLabel;  
  6. @property (nonatomic, strong, readonly) UILabel *subtitleLabel;  
  7. @property (nonatomic, strong, readwrite) UIColor *activityIndicatorViewColor NS_AVAILABLE_IOS(5_0);  
  8. @property (nonatomic, readwrite) UIActivityIndicatorViewStyle activityIndicatorViewStyle;  
  9.   
  10. @property (nonatomic, readonly) SVPullToRefreshState state;  
  11. @property (nonatomic, readonly) SVPullToRefreshPosition position;  
  12.   
  13. - (void)setTitle:(NSString *)title forState:(SVPullToRefreshState)state;  
  14. - (void)setSubtitle:(NSString *)subtitle forState:(SVPullToRefreshState)state;  
  15. - (void)setCustomView:(UIView *)view forState:(SVPullToRefreshState)state;  
  16.   
  17. - (void)startAnimating;  
  18. - (void)stopAnimating;  
  19.   
  20. // deprecated; use setSubtitle:forState: instead  
  21. @property (nonatomic, strong, readonly) UILabel *dateLabel DEPRECATED_ATTRIBUTE;  
  22. @property (nonatomic, strong) NSDate *lastUpdatedDate DEPRECATED_ATTRIBUTE;  
  23. @property (nonatomic, strong) NSDateFormatter *dateFormatter DEPRECATED_ATTRIBUTE;  
  24.   
  25. // deprecated; use [self.scrollView triggerPullToRefresh] instead  
  26. - (void)triggerRefresh DEPRECATED_ATTRIBUTE;  
  27.   
  28. @end  

SVPullToRefresh的实现用到了很多OC Runtime的特性。以前在用EGOTableViewPullRefresh时,我们需要

为当前的ViewController增加EGORefreshTableHeaderView的成员,还要实现相应protocol,这样ViewController就会显得臃肿。而SVPullToRefresh通过增加UIScrollView的Category,给需要下拉的scrollview增加了pullToRefreshView的属性,使用者不需要显式的创建它,与pullToRefreshView状态相关的逻辑也不用关心,只需要提供一个actionHandler来完成刷新结束后的一些操作。

但是category声明的property并不会自动@synthesize,所以需要手动实现getter和setter方法。通过runtime.h中objc_getAssociatedObject / objc_setAssociatedObject来访问和生成关联对象,从而模拟生成属性。如果要在不修改现有类的情况下增加属性,这是一个很好的办法。

[csharp] view plain copy
  1. - (void)setPullToRefreshView:(SVPullToRefreshView *)pullToRefreshView {  
  2.     [self willChangeValueForKey:@"SVPullToRefreshView"];  
  3.     objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,  
  4.                              pullToRefreshView,  
  5.                              OBJC_ASSOCIATION_ASSIGN);  
  6.     [self didChangeValueForKey:@"SVPullToRefreshView"];  
  7. }  
  8.   
  9. - (SVPullToRefreshView *)pullToRefreshView {  
  10.     return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView);  
  11. }  

除了动态增加属性,SVPullToRefresh还用到了KVO来观察自身属性的变化从而进行相应的UI调整(用EGOTableViewPullRefresh的话这些代码都由调用方实现,如

UIScrollViewDelegate

)。UIScrollView在滑动时contentOffset,contentSize,frame都是需要监测的对象。

[csharp] view plain copy
  1. - (void)setShowsPullToRefresh:(BOOL)showsPullToRefresh {  
  2.     self.pullToRefreshView.hidden = !showsPullToRefresh;  
  3.       
  4.     if(!showsPullToRefresh) {  
  5.         if (self.pullToRefreshView.isObserving) {  
  6.             [self removeObserver:self.pullToRefreshView forKeyPath:@"contentOffset"];  
  7.             [self removeObserver:self.pullToRefreshView forKeyPath:@"contentSize"];  
  8.             [self removeObserver:self.pullToRefreshView forKeyPath:@"frame"];  
  9.             [self.pullToRefreshView resetScrollViewContentInset];  
  10.             self.pullToRefreshView.isObserving = NO;  
  11.         }  
  12.     }  
  13.     else {  
  14.         if (!self.pullToRefreshView.isObserving) {  
  15.             [self addObserver:self.pullToRefreshView forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];  
  16.             [self addObserver:self.pullToRefreshView forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];  
  17.             [self addObserver:self.pullToRefreshView forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];  
  18.             self.pullToRefreshView.isObserving = YES;  
  19.               
  20.             CGFloat yOrigin = 0;  
  21.             switch (self.pullToRefreshView.position) {  
  22.                 case SVPullToRefreshPositionTop:  
  23.                     yOrigin = -SVPullToRefreshViewHeight;  
  24.                     break;  
  25.                 case SVPullToRefreshPositionBottom:  
  26.                     yOrigin = self.contentSize.height;  
  27.                     break;  
  28.             }  
  29.               
  30.             self.pullToRefreshView.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight);  
  31.         }  
  32.     }  
  33. }  


[csharp] view plain copy
  1. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {  
  2.     if([keyPath isEqualToString:@"contentOffset"])  
  3.         [self scrollViewDidScroll:[[change valueForKey:NSKeyValueChangeNewKey] CGPointValue]];  
  4.     else if([keyPath isEqualToString:@"contentSize"]) {  
  5.         [self layoutSubviews];  
  6.           
  7.         CGFloat yOrigin;  
  8.         switch (self.position) {  
  9.             case SVPullToRefreshPositionTop:  
  10.                 yOrigin = -SVPullToRefreshViewHeight;  
  11.                 break;  
  12.             case SVPullToRefreshPositionBottom:  
  13.                 yOrigin = MAX(self.scrollView.contentSize.height, self.scrollView.bounds.size.height);  
  14.                 break;  
  15.         }  
  16.         self.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight);  
  17.     }  
  18.     else if([keyPath isEqualToString:@"frame"])  
  19.         [self layoutSubviews];  
  20.   
  21. }  

总结:SVPullToRefresh把所有的逻辑都由自身实现,对外只提供一个Block接口,方便调用者。原理上和EGOTableViewPullRefresh差不多,但通过Runtime特性和KVO的运用,把原来客户端实现的逻辑放到了自身来实现,最终呈现一个简单的接口。
 类似资料: