iOS开发 下拉刷新上拉加载更多详解

下拉刷新(上拉加载更多)是大家经常用到的功能,本篇文章将带大家详细介绍下拉刷新原理,一步步实现下拉刷新效果。下拉刷新的核心原理是先自定义一个refreshView,然后将自定义的view添加到tableView(collectionView上)监听tableView(或者collectionView)的contentOffset属性,根据偏移量动态修改refreshView的子控件即可。下面一步步实现。
1.给UIScroolVIew添加一个分类UIScrollView+ZCRefresh.h,在该文件中添加如下代码:

//  UIScrollView+ZCRefresh.h  
//  ZCRefreshExample  
//  Created by MrZhao on 16/6/28.  
//  Copyright (c) 2016年 MrZhao. All rights reserved.  
#import <UIKit/UIKit.h>  
@class ZCHeaderRefreshView,ZCFooterRefreshView;  
@interface UIScrollView (ZCRefresh)  
/* 
 * 下拉刷新 
 */  
@property (nonatomic,strong)ZCHeaderRefreshView *zc_headerRefreshView;  
  
/* 
 * 上拉加载更多 
 */  
@property (nonatomic,strong)ZCFooterRefreshView *zc_footerRefreshView;  
@end  

在UIScrollView+ZCRefresh.m文件中添加如下代码:

//  UIScrollView+ZCRefresh.m  
//  ZCRefreshExample  
//  Created by MrZhao on 16/6/28.  
//  Copyright (c) 2016年 MrZhao. All rights reserved.  
#import "UIScrollView+ZCRefresh.h"  
#import "ZCHeaderRefreshView.h"  
#import "ZCFooterRefreshView.h"  
#import <objc/runtime.h>  
static const voidvoid *zc_headerRefresh_key = @"zc_headerRefresh_key";  
static const voidvoid *zc_footerRefresh_key = @"zc_footerRefresh_key";  
@implementation UIScrollView (ZCRefresh)  
  
#pragma mark 实现下拉刷新控件的get set 方法  
- (void)setZc_headerRefreshView:(ZCHeaderRefreshView *)zc_headerRefreshView {  
    if (zc_headerRefreshView != self.zc_headerRefreshView) {  
          
        //先删除旧的  
        [self.zc_headerRefreshView removeFromSuperview];  
        [self insertSubview:zc_headerRefreshView atIndex:0];        
        //添加新的  
        objc_setAssociatedObject(self, zc_headerRefresh_key, zc_headerRefreshView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);   
    }  
}  
- (ZCHeaderRefreshView *)zc_headerRefreshView {  
    return objc_getAssociatedObject(self, zc_headerRefresh_key);  
}  
- (void)setZc_footerRefreshView:(ZCFooterRefreshView *)zc_footerRefreshView {  
    if (zc_footerRefreshView != self.zc_footerRefreshView) {  
          
        [self.zc_footerRefreshView removeFromSuperview];  
        [self addSubview:zc_footerRefreshView];    
        //添加新的  
        objc_setAssociatedObject(self, zc_footerRefresh_key, zc_footerRefreshView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);     
    }  
}  
- (ZCFooterRefreshView *)zc_footerRefreshView {  
    return objc_getAssociatedObject(self, zc_footerRefresh_key);  
}  
@end

添加分类的目的是在使用时,可直接是self.tableView.zc_headerRefresh = ***,这样使用。其中用到了runtime里面的部分api,主要就是关联相关的api,大家不熟悉可以自行查找相关文档学习下。
2.自定义下拉刷新的View,ZCHeaderRefreshView.h,其中下拉的子控件和布局可以根据公司具体需求改动。ZCHeaderRefreshView.h中的代码如下。

//  ZCRefreshExample  
//  Created by MrZhao on 16/6/26.  
//  Copyright (c) 2016年 MrZhao. All rights reserved.  
//   git:https://github.com/MrZhaoCn/Refresh  
#import <UIKit/UIKit.h>  
#import <Foundation/Foundation.h>  
@interface ZCHeaderRefreshView : UIView  
/* 
 *提供有个工厂方法 
 */  
+ (instancetype)addHeaderRefreshViewWithTarget:(id)target action:(SEL)action;  
/* 
 *开始下拉刷新 
 */  
- (void)beginRefreshing;  
/* 
 *结束下拉刷新 
 */  
- (void)endHeaderRefreshing;  
/* 
 *设置下拉刷新相关图片 
 */  
//正常状态的图片  
- (void)setHeaderNormorlImageWithName:(NSString *)imageName;  
/* 
 *设置pullin状态关图片 
 */  
- (void)setHeaderPullingImageWithName:(NSString *)imageName;  
/* 
 *设置动画图片 
 */  
- (void)setAnimantionImages:(NSArray *)images;  
@end 

ZCHeaderRefreshView.m中的代码如下:


//  
//  ZCRefreshExample  
//  
//  Created by MrZhao on 16/6/26.  
//  Copyright (c) 2016年 MrZhao. All rights reserved.  
//  
  
#import "ZCHeaderRefreshView.h"  
#import <objc/message.h>  
#define ZCContentOffset  @"contentOffset"  
#define ScreenWidth  [UIScreen mainScreen].bounds.size.width  
const static int headerRefreshHeight = 60 ;  
/* 
 *枚举下拉刷新的几种状态 
 */  
typedef enum {  
    kZCStateNomorl = 0, //默认状态  
    kZCStatePulling,    //下拉状态  
    kZCStateRefreshing  //正在刷新状态  
} state;  
@interface ZCHeaderRefreshView ()  
/* 
 *用于设置图片 
 */  
@property (nonatomic,weak)UIImageView *imageView;  
/* 
 *用于设置文字 
 */  
@property (nonatomic,weak)UILabel *label;  
/* 
 *菊花控件 
 */  
@property (nonatomic,weak)UIActivityIndicatorView *activityView;  
/* 
 * 记录当前状态 
 */  
@property (nonatomic, assign)int currentState;  
/* 
 * 父控件 
 */  
@property (nonatomic,weak)UIScrollView *superview;  
/* 
 * 记录父控件初始时的偏移量,用于判定是否含有导航栏 
 */  
@property (nonatomic,assign)CGFloat contentOffSetY;  
/* 
 * 目标 
 */  
@property(nonatomic,weak)id target;  
/* 
 * 目标的方法,即刷新即将调用的方法 
 */  
@property(nonatomic,assign)SEL action;  
/* 
 *下拉刷新正常时的图片设置图片 
 */  
@property(nonatomic,strong)UIImage *headerNormoalImage;  
/* 
 *下拉时的图片设置图片 
 */  
@property(nonatomic,strong)UIImage *headerPullingImage;  
/* 
 *执行动画时的图片数组 
 */  
@property(nonatomic,strong)NSArray *animationImages;  
@end  
@implementation ZCHeaderRefreshView  
//工厂方法  
+ (instancetype)addHeaderRefreshViewWithTarget:(id)target action:(SEL)action {  
    ZCHeaderRefreshView *refreash = [[self alloc] init];  
    refreash.frame = CGRectMake(0, -headerRefreshHeight, ScreenWidth, headerRefreshHeight);  
    //背景颜色可根据需求设置或者取消  
    refreash.backgroundColor = [UIColor colorWithRed:230/255.0 green:230/255.0 blue:230/255.0 alpha:1.0];  
    refreash.currentState = kZCStateNomorl;  
    if (target != nil &&action != nil) {  
        refreash.target = target;  
        refreash.action = action;  
    }else {   
        NSLog(@"请设置刷新时调用的方法!!!");  
    }  
    return refreash;  
} 
#pragma mark子控件布局  
- (void)layoutSubviews {  
    [super layoutSubviews];
    //图片位置  
    CGFloat imagViewWH = 40;  
    //做了简单的适配  
    CGFloat imagViewX = ScreenWidth * 0.3;  
    self.imageView.frame = CGRectMake(  imagViewX, (self.frame.size.height - imagViewWH) / 2, imagViewWH, imagViewWH);  
    //文字位置  
    CGFloat labelX = CGRectGetMaxX(self.imageView.frame);  
    self.label.frame = CGRectMake(labelX , (self.frame.size.height - imagViewWH) / 2, 100, imagViewWH);  
    //菊花位置  
    self.activityView.frame = CGRectMake(  imagViewX, (self.frame.size.height - imagViewWH) / 2, imagViewWH, imagViewWH);  
}  
#pragma 加到父控件时会调用该方法  
- (void)willMoveToSuperview:(UIView *)newSuperview {  
    //是可以滚动的SCroolView才可以监听滚动事件  
    if ([newSuperview isKindOfClass:[UIScrollView class]]) {   
        //刷新控件添加的到的父控件  
        self.superview = (UIScrollView *)newSuperview;  
        //为父控件添加观察者,观察父控件的contentOffset.y值的变化。  
        [newSuperview addObserver:self forKeyPath:ZCContentOffset options:NSKeyValueObservingOptionNew context:nil];  
    }  
}  
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:  (NSDictionary *)change context:(voidvoid *)context {  
    if ([keyPath isEqualToString:ZCContentOffset]) {  
        [self adjustRefreshView];  
    }  
}  
#pragma 当正在操作下拉刷新控件时调用该方法  
- (void)adjustRefreshView {  
    //主要用来区别控制器有无导航栏  
    if (self.superview.contentInset.top == 64.000000) {  
        static dispatch_once_t one;  
        dispatch_once(&one, ^{  
            self.contentOffSetY = self.superview.contentInset.top;  
        });     
    }  
    CGFloat y = self.superview.contentOffset.y;  
    if (self.superview.isDragging) { //正在拖动  
        if (y<  -self.contentOffSetY &&y> -self.contentOffSetY - headerRefreshHeight && self.currentState ==kZCStatePulling) { //正常状态->下拉  
            self.currentState = kZCStateNomorl;        
        }else if (y <= -self.contentOffSetY - headerRefreshHeight && self.currentState == kZCStateNomorl)//下拉->正常  
        {  
            self.currentState = kZCStatePulling;  
        }  
    }else if(self.currentState ==kZCStatePulling &&y <= -self.contentOffSetY - headerRefreshHeight) { //手释放      
        self.currentState = kZCStateRefreshing;  
    }  
}  
#pragma 重写setState方法,在该方法中修改文字,图片  
    if (_currentState == currentState) { //相等直接返回  
        return;  
    }  
    _currentState = currentState;  
    if (_currentState ==kZCStateNomorl) {//默认状态什么都不  
          
        self.imageView.hidden = NO;  
        [self.activityView stopAnimating];  
        self.activityView.hidden = YES;  
        [UIView animateWithDuration:0.5 animations:^{  
            [self.imageView stopAnimating];  
            self.label.text = @"下拉刷新";  
            //如果没有设置动画图片  
            if (self.animationImages == nil)  {  
                if (self.headerNormoalImage == nil) {//没有设置正常图片,则采用默认的  
                    self.imageView.image = [UIImage imageNamed:@"down"];  
                }   else {//采用设置的图片  
                    self.imageView.image = self.headerNormoalImage;  
                }  
            } else {//   如果设置了动画,则采用第一张做正常时的图片  
                self.imageView.image = self.animationImages[0];       
            }  
        }];        
    }else if (_currentState ==kZCStatePulling){//下拉状态  
        self.imageView.hidden = NO;  
        [self.activityView stopAnimating];  
        self.activityView.hidden = YES;  
        [UIView animateWithDuration:0.5 animations:^{     
            [self.imageView stopAnimating];  
            self.label.text= @"释放立即刷新";  
            //如果没有设置动画图片  
            if (self.animationImages == nil)  {  
                if (self.headerPullingImage == nil) {//没有设置下拉图片,则采用默认的  
                    self.imageView.image = [UIImage imageNamed:@"up"];  
                } else {//采用设置的图片  
                    self.imageView.image = self.headerPullingImage;  
                }         
            } else {  
                //   如果设置了动画,则采用第一张做下拉时的图片  
                self.imageView.image = self.animationImages[0];  
            }  
        }];  
    } else if (_currentState == kZCStateRefreshing){ //释放刷新  
        self.label.text = @"正在刷新...";  
        //没有动画图片,默认采用菊花控件  
        if (self.animationImages == nil) {  
            self.activityView.hidden = NO;  
            self.imageView.hidden = YES;  
            [self.activityView startAnimating];       
        } else {  
            self.imageView.hidden = NO;  
            self.activityView.hidden = YES;  
            self.imageView.animationDuration = 0.1 * self.animationImages.count;  
            [self.imageView startAnimating];       
        }  
        //放手之后不能立即返回  
        [UIView animateWithDuration:0.25 animations:^{  
            self.superview.contentInset = UIEdgeInsetsMake(self.superview.contentInset.top + headerRefreshHeight, self.superview.contentInset.left, self.superview.contentInset.bottom, self.superview.contentInset.right);  
        }];  
        //不能直接调用objec.msgSend()  
        void (*action)(id, SEL) = (void (*)(id, SEL)) objc_msgSend;  
        action(self.target,self.action);      
    }  
}  
#pragma 开始刷新  
- (void)beginRefreshing {   
    self.currentState = kZCStateRefreshing;  
}  
#pragma 结束下拉刷新  
- (void)endHeaderRefreshing {     
    if (self.currentState == kZCStateRefreshing) {  
        self.currentState = kZCStateNomorl;  
        [UIView animateWithDuration:0.25 animations:^{  
            self.superview.contentInset = UIEdgeInsetsMake(self.superview.contentInset.top - headerRefreshHeight, self.superview.contentInset.left, self.superview.contentInset.bottom, self.superview.contentInset.right);  
        }];  
    }  
}  
#pragma mark 一定要记得移除观察者,不然会崩  
- (void)dealloc  {  
    [self.superview removeObserver:self forKeyPath:ZCContentOffset];  
}  
#pragma mark 设置图片相关方法  
- (void)setHeaderNormorlImageWithName:(NSString *)imageName {  
    self.headerNormoalImage = [UIImage imageNamed:imageName];  
}  
- (void)setHeaderPullingImageWithName:(NSString *)imageName {   
    self.headerPullingImage = [UIImage imageNamed:imageName];  
}  
- (void)setAnimantionImages:(NSArray *)images {    
    self.animationImages = images;  
    self.imageView.animationImages = self.animationImages;  
}  
#pragma mark懒加载子控件,放到最后这样不影响主逻辑  
// 1 图片控件  
- (UIImageView *)imageView {  
    if (_imageView == nil) {    
        UIImageView *imageView = [[UIImageView alloc] init];  
        //如果没有设置动画图片  
        if (self.animationImages == nil)  
        {  
            if (self.headerNormoalImage == nil) {//没有设置正常图片,则采用默认的  
                imageView.image = [UIImage imageNamed:@"down"];  
            }  
            else {//采用设置的图片  
                imageView.image = self.headerNormoalImage;  
            }         
        }else {//   如果设置了动画,则采用第一张做正常时的图片  
            imageView.image = self.animationImages[0];  
        }  
        imageView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; 
        [self addSubview: imageView];  
        _imageView = imageView;  
    }  
    return _imageView;  
}   
//2 文本控件  
- (UILabel *)label {  
    if (_label == nil) { 
        //2 文本控件  
        UILabel *label = [[UILabel alloc] init];  
        label.textColor = [UIColor darkGrayColor];  
        label.font = [UIFont systemFontOfSize:13];  
        label.backgroundColor = [UIColor clearColor];  
        label.textAlignment = NSTextAlignmentCenter;  
        label.text = @"下拉刷新";  
        [label sizeToFit];  
        [self addSubview:label];  
        _label = label;  
    }  
    return _label;  
} 
//菊花控件  
- (UIActivityIndicatorView *)activityView {  
    if (_activityView == nil) {  
        UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] init];  
        self.activityView = activityView;  
        activityView.bounds = self.imageView.bounds;  
        activityView.autoresizingMask = self.imageView.autoresizingMask;  
        [self addSubview: activityView];   
    }  
    return _activityView;  
}  
@end  

说明:
1)子控件的多少根位置可根据需求改变。
2)在控制器里面self.tableView.zc_headerRefresh = ***这段代码时会调用willMoveToSuperView这个方法,在这个方法中就可以拿到父视图tableView(collectionView),给tableView(collectionView)添加contOffSet观察者。
3)根据contOffSet的变化,不断调整刷新控件里面子控件的状态,包括切换图片,动图等等.
4)注意在不同状态下要调整tableView的contInset属性.
3.在控制器里面可以这样用:

//添加下拉刷新控件  
    ZCHeaderRefreshView *refreshView = [ZCHeaderRefreshView addHeaderRefreshViewWithTarget:self action:@selector(loadNewDataSoure)];  
      
    //如果没有设置动画则采用默认的菊花转,且下拉和正常状态图片由代码提供,若果不提供,则采用默认图片,  
    //如果设置了动画,则用动画转动,且下拉,和正常状态的图片采用动画的第一张图片。  
    UIImage *image1 = [UIImage imageNamed:@"icon_listheader_animation_1"];  
    UIImage *image2 = [UIImage imageNamed:@"icon_listheader_animation_2"];  
    NSArray *animationImages = @[image1,image2];  
    [refreshView setAnimantionImages:animationImages];  
    self.tableView.zc_headerRefreshView = refreshView;  
    [self.tableView.zc_headerRefreshView beginRefreshing]; 

好了,以上就是下拉刷新的基本写法,下拉加载更多原理跟上拉加载更多类似,大家自行看代码。
代码地址:https://github.com/MrZhaoCn/ZCRefresh
仓库里还有直播类的开源项目,自动计算cell高度的代码,欢迎大家start.

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

推荐阅读更多精彩内容