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 
@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 

static const void *zc_headerRefresh_key = @"zc_headerRefresh_key";
static const void *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 
#import 
@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 
#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:(void *)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方法,在该方法中修改文字,图片
- (void)setCurrentState:(int)currentState {
    
    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.

iOS开发 下拉刷新上拉加载更多详解_第1张图片iOS开发 下拉刷新上拉加载更多详解_第2张图片iOS开发 下拉刷新上拉加载更多详解_第3张图片

你可能感兴趣的:(iOS)