MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的下拉刷新与上拉加载实现

2021-08-126.5k 阅读

一、下拉刷新与上拉加载的应用场景及重要性

在移动应用开发中,下拉刷新与上拉加载是极为常见且重要的交互模式。对于以内容展示为主的应用,如下载量极高的社交媒体应用,用户期望能够快速获取最新动态,下拉刷新功能便成为满足这一需求的关键。当用户在浏览好友动态列表时,通过简单的向下拉动操作,即可刷新获取最新发布的内容,保持信息的实时性。

而上拉加载则主要用于处理大量数据的分页展示。以新闻资讯类应用为例,新闻数量众多,若一次性全部加载,不仅耗费大量流量,还会导致应用响应缓慢。通过上拉加载,当用户滚动到列表底部时,自动加载下一页内容,有效提升了用户体验,避免用户在等待漫长加载过程中的烦躁情绪。在 Objective - C 开发的 iOS 应用里,实现这两个功能对于提升用户满意度、增强应用的实用性和吸引力具有不可或缺的作用。

二、Objective - C 实现下拉刷新

(一)基于 UIScrollView 的下拉刷新原理

在 iOS 开发中,UIScrollView 是众多视图容器类的基础,UITableViewUICollectionView 都继承自 UIScrollView。下拉刷新功能的实现核心便是利用 UIScrollView 的滚动属性及事件。当用户下拉 UIScrollView 时,我们可以监听其偏移量变化。正常情况下,UIScrollViewcontentOffset.y 值为 0 或正数,但当用户用力下拉时,contentOffset.y 会变为负数。通过监测这个负数的变化范围,我们可以判断用户是否执行了下拉刷新操作,并在合适的时机触发刷新逻辑。

(二)使用第三方库 MJRefresh 实现下拉刷新

  1. MJRefresh 库介绍 MJRefresh 是一款在 iOS 开发中广泛使用的下拉刷新和上拉加载库,由国人开发,具有高度的可定制性和易用性。它为开发者提供了简洁的 API,极大地简化了下拉刷新和上拉加载的实现过程。
  2. 安装 MJRefresh 安装 MJRefresh 通常使用 CocoaPods 进行。在项目的 Podfile 文件中添加如下代码:
pod 'MJRefresh'

然后在终端执行 pod install 命令,CocoaPods 会自动下载并集成 MJRefresh 到项目中。 3. 代码实现 假设我们有一个 UITableView 需要实现下拉刷新功能,在视图控制器的 viewDidLoad 方法中添加如下代码:

#import "ViewController.h"
#import "MJRefresh.h"

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *dataArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.dataArray = [NSMutableArray array];
    // 初始化数据,假设这里从网络获取了一些初始数据
    [self.dataArray addObjectsFromArray:@[@"item1", @"item2", @"item3"]];
    
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.view addSubview:self.tableView];
    
    // 添加下拉刷新
    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshData)];
}

- (void)refreshData {
    // 模拟网络请求获取新数据
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 假设获取到了新数据
        NSArray *newData = @[@"newItem1", @"newItem2"];
        [self.dataArray insertObjects:newData atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newData.count)]];
        [self.tableView reloadData];
        // 结束刷新
        [self.tableView.mj_header endRefreshing];
    });
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
    }
    cell.textLabel.text = self.dataArray[indexPath.row];
    return cell;
}

@end

在上述代码中,首先在 viewDidLoad 方法里初始化了 UITableView 并添加了 MJRefresh 的下拉刷新头部视图 MJRefreshNormalHeader。当触发下拉刷新时,会调用 refreshData 方法。在这个方法里,通过 dispatch_after 模拟了网络请求,获取新数据后更新数据源并重新加载表格,最后调用 [self.tableView.mj_header endRefreshing] 结束刷新状态。

(三)手动实现下拉刷新

  1. 创建下拉刷新视图 手动实现下拉刷新需要我们自己创建一个用于展示刷新状态的视图,通常包含一个指示器(如旋转的菊花指示器)和提示文字。以一个简单的自定义视图 PullToRefreshView 为例,其头文件 PullToRefreshView.h 代码如下:
#import <UIKit/UIKit.h>

@interface PullToRefreshView : UIView

@property (nonatomic, strong) UIActivityIndicatorView *activityIndicator;
@property (nonatomic, strong) UILabel *statusLabel;

- (void)startAnimating;
- (void)stopAnimating;

@end

实现文件 PullToRefreshView.m 代码如下:

#import "PullToRefreshView.h"

@implementation PullToRefreshView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        self.activityIndicator.frame = CGRectMake((self.bounds.size.width - 20) / 2, (self.bounds.size.height - 20) / 2, 20, 20);
        [self addSubview:self.activityIndicator];
        
        self.statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, CGRectGetMaxY(self.activityIndicator.frame) + 10, self.bounds.size.width, 20)];
        self.statusLabel.textAlignment = NSTextAlignmentCenter;
        self.statusLabel.font = [UIFont systemFontOfSize:14];
        [self addSubview:self.statusLabel];
    }
    return self;
}

- (void)startAnimating {
    [self.activityIndicator startAnimating];
    self.statusLabel.text = @"正在刷新...";
}

- (void)stopAnimating {
    [self.activityIndicator stopAnimating];
    self.statusLabel.text = @"下拉刷新";
}

@end
  1. 在视图控制器中集成 在视图控制器中,我们需要将这个自定义的 PullToRefreshViewUIScrollView 关联起来,并监听 UIScrollView 的滚动事件。以下是视图控制器部分代码:
#import "ViewController.h"
#import "PullToRefreshView.h"

@interface ViewController () <UIScrollViewDelegate>

@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) PullToRefreshView *refreshView;
@property (nonatomic, assign) BOOL isRefreshing;
@property (nonatomic, strong) NSMutableArray *dataArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.dataArray = [NSMutableArray array];
    // 初始化数据,假设这里从网络获取了一些初始数据
    [self.dataArray addObjectsFromArray:@[@"item1", @"item2", @"item3"]];
    
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.delegate = self;
    [self.view addSubview:self.scrollView];
    
    self.refreshView = [[PullToRefreshView alloc] initWithFrame:CGRectMake(0, -self.view.bounds.size.height * 0.2, self.view.bounds.size.width, self.view.bounds.size.height * 0.2)];
    [self.scrollView addSubview:self.refreshView];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGFloat offsetY = scrollView.contentOffset.y;
    if (offsetY < 0 &&!self.isRefreshing) {
        CGFloat progress = -offsetY / (self.view.bounds.size.height * 0.2);
        if (progress >= 1) {
            self.refreshView.statusLabel.text = @"松开立即刷新";
        } else {
            self.refreshView.statusLabel.text = @"下拉刷新";
        }
    }
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    CGFloat offsetY = scrollView.contentOffset.y;
    if (offsetY < -self.view.bounds.size.height * 0.2 &&!self.isRefreshing) {
        self.isRefreshing = YES;
        [self.refreshView startAnimating];
        // 模拟网络请求获取新数据
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 假设获取到了新数据
            NSArray *newData = @[@"newItem1", @"newItem2"];
            [self.dataArray insertObjects:newData atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newData.count)]];
            // 重新布局数据展示
            // 这里假设是简单的重新计算滚动视图内容大小
            CGFloat newContentHeight = self.dataArray.count * 44;
            self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, newContentHeight);
            [self.refreshView stopAnimating];
            self.isRefreshing = NO;
            self.scrollView.contentOffset = CGPointMake(0, 0);
        });
    }
}

@end

在上述代码中,scrollViewDidScroll 方法用于根据滚动偏移量更新 PullToRefreshView 的提示文字。scrollViewDidEndDragging 方法则判断用户是否下拉到了足以触发刷新的位置,如果是,则开始刷新动画并模拟网络请求,获取新数据后更新数据源及重新布局滚动视图内容,并结束刷新动画。

三、Objective - C 实现上拉加载

(一)基于 UIScrollView 的上拉加载原理

上拉加载同样基于 UIScrollView 的滚动监测。当 UIScrollViewcontentOffset.y 值加上 UIScrollView 的高度接近或等于 contentSize.height 时,意味着用户已经滚动到了列表底部。此时,我们可以触发加载更多数据的逻辑,通过网络请求获取下一页数据,并将其添加到数据源中,然后更新视图展示。

(二)使用 MJRefresh 实现上拉加载

  1. 代码实现 继续使用之前的 UITableView 示例,在 viewDidLoad 方法中添加上拉加载代码如下:
#import "ViewController.h"
#import "MJRefresh.h"

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *dataArray;
@property (nonatomic, assign) NSInteger page;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.page = 1;
    self.dataArray = [NSMutableArray array];
    // 初始化数据,假设这里从网络获取了第一页数据
    [self.dataArray addObjectsFromArray:@[@"item1", @"item2", @"item3"]];
    
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.view addSubview:self.tableView];
    
    // 添加下拉刷新
    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshData)];
    
    // 添加上拉加载
    self.tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreData)];
}

- (void)refreshData {
    self.page = 1;
    // 模拟网络请求获取第一页新数据
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 假设获取到了新数据
        NSArray *newData = @[@"newItem1", @"newItem2"];
        [self.dataArray removeAllObjects];
        [self.dataArray addObjectsFromArray:newData];
        [self.tableView reloadData];
        // 结束刷新
        [self.tableView.mj_header endRefreshing];
    });
}

- (void)loadMoreData {
    self.page++;
    // 模拟网络请求获取下一页数据
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 假设获取到了新数据
        NSArray *newData = @[@"moreItem1", @"moreItem2"];
        [self.dataArray addObjectsFromArray:newData];
        [self.tableView reloadData];
        // 结束加载
        [self.tableView.mj_footer endRefreshing];
    });
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
    }
    cell.textLabel.text = self.dataArray[indexPath.row];
    return cell;
}

@end

在上述代码中,mj_footer 即为 MJRefresh 提供的上拉加载尾部视图 MJRefreshAutoNormalFooter。当触发上拉加载时,会调用 loadMoreData 方法。在这个方法里,首先增加页码 page,然后模拟网络请求获取下一页数据,添加到数据源并重新加载表格,最后调用 [self.tableView.mj_footer endRefreshing] 结束加载状态。

(三)手动实现上拉加载

  1. 监测滚动到列表底部 手动实现上拉加载需要在视图控制器中监听 UIScrollView 的滚动事件,判断是否滚动到了底部。以下是相关代码片段:
#import "ViewController.h"

@interface ViewController () <UIScrollViewDelegate>

@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) NSMutableArray *dataArray;
@property (nonatomic, assign) NSInteger page;
@property (nonatomic, assign) BOOL isLoading;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.page = 1;
    self.dataArray = [NSMutableArray array];
    // 初始化数据,假设这里从网络获取了第一页数据
    [self.dataArray addObjectsFromArray:@[@"item1", @"item2", @"item3"]];
    
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.delegate = self;
    [self.view addSubview:self.scrollView];
    // 假设每个子视图高度为 44
    CGFloat contentHeight = self.dataArray.count * 44;
    self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, contentHeight);
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGFloat offsetY = scrollView.contentOffset.y;
    CGFloat scrollViewHeight = scrollView.bounds.size.height;
    CGFloat contentHeight = scrollView.contentSize.height;
    if (offsetY + scrollViewHeight >= contentHeight - 10 &&!self.isLoading) {
        self.isLoading = YES;
        [self loadMoreData];
    }
}

- (void)loadMoreData {
    self.page++;
    // 模拟网络请求获取下一页数据
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 假设获取到了新数据
        NSArray *newData = @[@"moreItem1", @"moreItem2"];
        [self.dataArray addObjectsFromArray:newData];
        // 重新计算滚动视图内容大小
        CGFloat newContentHeight = self.dataArray.count * 44;
        self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, newContentHeight);
        self.isLoading = NO;
    });
}

@end

scrollViewDidScroll 方法中,通过判断 contentOffset.y 加上 UIScrollView 的高度是否接近或等于 contentSize.height(这里减去 10 是为了提前触发加载,避免用户已经看到底部空白才加载),如果满足条件且当前没有正在加载数据(!self.isLoading),则开始加载更多数据。在 loadMoreData 方法中,增加页码,模拟网络请求获取新数据,更新数据源并重新计算 UIScrollViewcontentSize

四、优化与注意事项

(一)优化网络请求

无论是下拉刷新还是上拉加载,网络请求都是关键部分。为了提升用户体验,应尽量减少网络请求的时间。可以采用以下方法:

  1. 数据缓存:对于频繁请求且数据变化不频繁的接口,使用缓存机制。例如,在下拉刷新时,先检查本地缓存,如果缓存未过期,则直接使用缓存数据展示给用户,同时在后台发起网络请求更新缓存。可以使用 NSURLCache 来实现简单的 HTTP 缓存。以下是一个简单示例:
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:@"myCache"];
[NSURLCache setSharedURLCache:cache];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"yourURL"]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
    if (data) {
        // 处理数据
    }
}];
  1. 优化请求参数:确保每次请求携带的参数都是必要的,避免发送过多冗余数据,减少请求体大小,从而加快请求速度。

(二)用户体验优化

  1. 加载动画优化:对于下拉刷新和上拉加载的动画,应保证其流畅性和美观性。例如,在使用自定义视图实现下拉刷新时,要确保指示器的旋转动画平滑,提示文字的更新及时且准确。对于上拉加载,可以在加载过程中显示一个简单的加载提示视图,如“加载更多中...”,避免用户在等待过程中产生疑惑。
  2. 避免重复加载:在实现过程中要注意避免重复加载的情况。例如,当上拉加载正在进行时,用户再次滚动到列表底部,不应再次触发加载逻辑。可以通过设置一个标志位(如上述代码中的 isLoading)来判断当前是否正在加载数据,避免重复请求。

(三)内存管理

  1. 数据源管理:在频繁更新数据源(如下拉刷新和上拉加载时),要注意内存的释放。如果使用 NSMutableArray 作为数据源,在更新数据时,确保不再使用的对象被正确释放。例如,在下拉刷新时,如果需要清空旧数据并重新加载新数据,应使用 removeAllObjects 方法清理数组,避免旧数据一直占用内存。
  2. 视图重用:对于 UITableViewUICollectionView,要充分利用其提供的视图重用机制。在 cellForRowAtIndexPath: 方法中,通过 dequeueReusableCellWithIdentifier: 方法获取可重用的单元格,避免频繁创建新的单元格,从而减少内存开销。

通过以上对 Objective - C 中下拉刷新与上拉加载的实现、优化及注意事项的详细阐述,开发者可以在 iOS 应用开发中更好地实现这两个重要功能,为用户提供更加流畅、高效的应用体验。无论是使用第三方库还是手动实现,都需要根据项目的具体需求和特点进行选择和优化。