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閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容