iOS--利用UITableViewDataSourcePrefetching实现平滑如丝的无限滚动

前言:

相信大家在网络不好的时候使用列表分页的App会获得非常不好的体验,由于网络的问题,会有明显的卡顿,就像抖音等App,那么我们是否能使用一些手段来优化这个体验呢?这里可以用到UITableView中另一个协议:UITableViewDataSourcePrefetching。从而实现平滑如斯的无限滚动。

什么是UITableViewDataSourcePrefetching?

Prefetching API提供了一种在需要显示数据之前预先准备数据的机制,旨在提高数据的滚动性能。

首先,先和大家介绍一个概念:无限滚动,无限滚动是可以让用户连续的加载内容,而无需分页。在 UI 初始化的时候 App 会加载一些初始数据,然后当用户滚动快要到达显示内容的底部时加载更多的数据。

如何实现?

这里简单说一下思路:

先自定义一个 Cell 视图,这个视图由一个 UILabel 和 一个 UIImageView 构成,用于显示猫咪品种文本和猫咪网络图片;然后网络请求来获取数据,注意该步骤一定是异步执行的;最后用 UITableView 来显示返回的数据,在 viewDidLoad 中先请求网络数据来获取一些初始化数据,然后再利用 UITableView 的 Prefetching API 来对数据进行预加载,从而来实现数据的无缝加载。

其次,这里由于本地运行的局限性,无限滚动并不是可以真正的加载无限的数据,我这里设置的数据最大值为1000,也就是说最多能加载1000个cell,事实上想要实现无限滚动需要一个庞大的服务器数据做支撑。

具体步骤:

我找到的一个免费的分页接口测试网站,在这个网站上注册后可以获取一定额度的次数,已经足够了。网站如下:https://portal.thatapicompany.com/the-api-marketplace/the-cat-api/

OK接下来就是利用这个网站请求网络数据了,不熟悉网络请求的可以看我这一篇博客:iOS--NSURLSession && Alamofire流程源码解析(万字详解版)-CSDN博客

我这里同样是用的第三方库Alamofire。

HTTP demo如下:

import Foundation
import Alamofire//网球请求 每次获取30张猫咪照片和名字数据
extension ViewController{func CatRequest(){// 请求猫数据guard !isFetching else { return }isFetching = truelet catUrl = "https://api.thecatapi.com/v1/images/search?size=med&mime_types=jpg&format=json&has_breeds=true&order=RANDOM&page=\(1)&limit=\(limit)"DispatchQueue.main.async {AF.request(catUrl, method: .get, headers: headers).responseDecodable(of: [CatImage].self) { response inswitch response.result {case .success(let catImages):print(catImages)catImagesArray.append(contentsOf: catImages)isFetching = falseif(self.count >= 1){//获取将要加载的区间,利用tableview.reloadRows加载的时候只加载这一区间即可,防止整体重新加载导致性能不佳let statrIndex = catImagesArray.count - catImages.countlet endIndex = catImagesArray.count + catImages.count - 1var newIndexPaths : [IndexPath] = []for i in statrIndex...endIndex{newIndexPaths.append(IndexPath(row: i, section: 0))}let indexPathsToReload = self.visibleIndexPathsToReload(intersecting: newIndexPaths)self.tableView.beginUpdates()self.tableView.reloadRows(at: indexPathsToReload, with: .automatic)self.tableView.endUpdates()}else{//第一次加载的时候只有30个cell 直接全部加载self.tableView.reloadData()self.tableView.tableFooterView = nil}self.count += 1case .failure(let error):print("Error fetching data: \(error)")isFetching = false}}}}
}

这里有个优化的思想,就是先获取需要加载的那部分区间,然后tableview直接加载那部分区间即可,防止整体重新加载导致性能不佳。

接着我定义了一些model:

//定义猫咪模型
struct CatImage : Codable {let url : Stringlet breeds : [BREEDS]
}
struct BREEDS : Codable{let name : String
}let headers : HTTPHeaders = ["Content-Type" : "application/json","x-api-key" : "YOUR_API_KEY"
]
let tableID = "catTableID"
var catImagesArray : [CatImage] = []
let limit: Int = 30
var isFetching: Bool = false
//缓存 避免重复下载
let imageCache = NSCache<NSURL, UIImage>()

其中imageCache是用来缓存的,至于为什么要缓存,后面会讲到的。

以下是我自定义的cell:

class CatTableViewCell: UITableViewCell {let catImageView = UIImageView()let nameLabel = UILabel()var loadingIndicator: UIActivityIndicatorView?override func prepareForReuse() {super.prepareForReuse()// 避免 cell 重用导致数据重叠nameLabel.text = ""catImageView.image = .none}override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {super.init(style: style, reuseIdentifier: reuseIdentifier)self.contentView.backgroundColor = .darkGray// 配置猫咪图片catImageView.contentMode = .scaleAspectFillcatImageView.layer.cornerRadius = 10catImageView.clipsToBounds = truecatImageView.translatesAutoresizingMaskIntoConstraints = falseself.contentView.addSubview(catImageView)// 配置名字标签nameLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium)nameLabel.textColor = .whitenameLabel.translatesAutoresizingMaskIntoConstraints = falseself.contentView.addSubview(nameLabel)// 添加 AutoLayout 约束NSLayoutConstraint.activate([// 猫咪图片约束catImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),catImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),catImageView.widthAnchor.constraint(equalToConstant: 60),catImageView.heightAnchor.constraint(equalToConstant: 60),// 猫咪名字标签约束nameLabel.leadingAnchor.constraint(equalTo: catImageView.trailingAnchor, constant: 15),nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)])loadingIndicator = UIActivityIndicatorView(frame: self.frame)loadingIndicator?.center = self.contentView.centerloadingIndicator?.color = .whiteself.contentView.addSubview(loadingIndicator!)}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}
}extension CatTableViewCell{// 配置单元格内容func configure(with catImage: CatImage, indexPath: IndexPath) {// 先显示加载指示器loadingIndicator?.startAnimating()catImageView.image = nilnameLabel.text = ""// 异步加载图片DispatchQueue.main.async {if let imageUrl = URL(string: catImage.url) {if catImagesArray[indexPath.row].url == catImage.url {self.loadImage(from: imageUrl) { [weak self] image inDispatchQueue.main.async {// 确保此时 indexPath 没有被重用if catImagesArray[indexPath.row].url == catImage.url {self?.catImageView.image = imageself?.loadingIndicator?.stopAnimating()}}}}}// 设置名字标签if let breed = catImage.breeds.first {self.nameLabel.text = breed.name} else {self.nameLabel.text = ""}}}
}extension CatTableViewCell{//URL 转 imagefunc loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {// 检查缓存,避免重复下载if let cachedImage = imageCache.object(forKey: url as NSURL) {completion(cachedImage)return}// 异步加载图片let task = URLSession.shared.dataTask(with: url) { data, response, error inif let data = data, let image = UIImage(data: data) {// 缓存图片imageCache.setObject(image, forKey: url as NSURL)completion(image)} else {completion(nil)}}task.resume()}
}

这里获取到的猫咪图片是URL格式,需要转成UIImage,configure()中需要注意一个点:就是不能用弱引用,如果 self 被弱引用,可能会导致在任务完成之前 self 被释放,导致界面无法正确更新,就会造成猫咪图片时常更换刷新的问题。

此外,loadImage()这里用到了缓存机制,如果在缓存中找到了之前加载的图片的话,就不用在重复下载图片,节省了开销。

网络数据回调处理:

extension ViewController : UITableViewDelegate,UITableViewDataSource{func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {return 1000}func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {let cell = tableView.dequeueReusableCell(withIdentifier: tableID, for: indexPath) as! CatTableViewCell// 重置单元格内容,避免重用导致错位问题cell.prepareForReuse()//先不加载图片 而是先加载cell 哪怕没有返回图片 保持一直滚动的状态if(indexPath.row < catImagesArray.count){cell.configure(with: catImagesArray[indexPath.row],indexPath: indexPath)}else{cell.configure(with: .init(url: "", breeds: []),indexPath: .init())}return cell}func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {return 100.0}
}

因为是图片加载,因为tableview的复用机制会导致前面的cell再次被复用,这里用重制单元格内容和直接赋值的方法处理。此外,由于网络请求的滞后性,我们先加载cell,这个cell是没有内容的,然后等返回网络数据的时候再将这个区间的cell刷新一遍即可。从而实现一直滚动的功能。

那么,如何实现预加载呢?

这里的预加载,其实换句话说就是,如果在适当的时候进行网络呢?网络请求早了不行,会导致用户和数据的“供过于求”,网络请求晚了又会造成“供不应求”。恰当的利用prefetchDataSource可以解决这个问题。

首先让当前的 ViewController 遵循 UITableViewDataSourcePrefetching 协议:

class ViewController: UIViewController,UITableViewDataSourcePrefetching{var count : Int = 0lazy var tableView : UITableView = {let tableview = UITableView(frame: self.view.bounds)/...tableview.prefetchDataSource = self/...return tableview}()override func viewDidLoad() {super.viewDidLoad()self.view.backgroundColor = .blueself.view.addSubview(tableView)CatRequest()}
}

然后我们来了解下UITableViewDataSourcePrefetching,它的协议里包含俩个函数:

// this protocol can provide information about cells before they are displayed on screen.@protocol UITableViewDataSourcePrefetching <NSObject>@required// indexPaths are ordered ascending by geometric distance from the table view
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;@optional// indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -tableView:prefetchRowsAtIndexPaths:
- (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;@end
  • prefetchRowsAtIndexPaths函数会基于当前滚动的方向和速度对接下来的 IndexPaths 进行 Prefetch,通常我们会在这里实现预加载数据的逻辑。
  • cancelPrefetchingForRowsAtIndexPaths函数是一个可选的方法,当用户快速滚动导致一些 Cell 不可见的时候,你可以通过这个方法来取消任何挂起的数据加载操作,有利于提高滚动性能, 这里我没有使用过,但它是很好的性能优化帮手。

UITableViewDataSourcePrefetching如何工作:

1. 当用户快速滚动表格时,表格会调用 tableView(_:prefetchRowsAt:) 方法,它提供了一个即将显示的 IndexPath 列表。

2. 你可以在这个方法中,提前为这些 IndexPath 加载数据,比如发起网络请求、从缓存中获取数据、处理图片等。

3. 当用户真正滚动到这些单元格时,数据就已经准备好了,因此能立即显示,而不会造成卡顿或延迟。

实现逻辑:
    // Prefetching: 提前加载即将显示的数据func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {//返回一个布尔值,表示序列是否包含满足给定谓词的元素。这里是查看是否有创建的cell没有获取加载的猫咪图片let needFetch = indexPaths.contains { isLoadingCell(for: $0)}if needFetch {// 1.满足条件进行网络请求CatRequest()}}//你获取到新的数据,并且有部分数据需要更新时,你只会刷新那些当前在屏幕上可见的单元格,避免刷新不在屏幕上可见的单元格,提升性能。func visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {//会获取当前表格视图中所有可见行的 IndexPath 数组(即正在屏幕上显示的单元格的索引路径)let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)//使用 Set 来将可见行的 IndexPath 集合化,并与传入的 indexPaths 进行 交集运算,找出既在 indexPaths 中,又是当前可见的行。这个操作是为了只重新加载那些当前可见且需要更新的单元格,避免无关单元格的重复加载。return Array(indexPathsIntersection)}//当滑动的单元格大于等于加载的图片 则一直创造单元格func isLoadingCell(for indexPath: IndexPath) -> Bool {return indexPath.row >= (catImagesArray.count - 15)}

经过上述的设置后,我们就实现了以下效果:

总结:

  1. 我们用prefetchDataSource 提升用户体验,当用户快速滚动表格时,它能够提前加载数据,避免因加载数据而导致的卡顿。其中我们可以在prefetchRowsAtIndexPaths函数中编写逻辑。这里逻辑包括了isLoadingCell和visibleIndexPathsToReload等一系列方法。
  2. 为了优化tableview的性能,我们每次获取到网络数据的时候,并不使用tableview.reloadData(),而是通过返回数据的数组catImagesArray和获取到的catImages之间的数量逻辑计算出需要刷新加载区间的cell,使用self.tableView.reloadRows(at: indexPathsToReload, with: .automatic)这部分区间即可。
  3. 有时候弱引用并没有想象中的那么好用,在编写项目的时候,我习惯性的在写cell的configure函数的时候使用了弱引用,从而造成了cell的图片不断刷新的这一bug。
  4. 缓存的思想:每次网络请求返回的图片URL我们需要转换成UIImage,这时候我们可以将其转换的数据缓存,避免重复转换。
  5. 为了防止tableview的复用机制踩坑,我们使用了prepareForReuse()这一方法在单元格被重用之前对其进行重置。

按照国际惯例,最后附上项目工程地址:https://github.com/iOSwhj/TheCatTableView

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/455750.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux Debian12基于ImageMagick图像处理工具编写shell脚本用于常见图片png、jpg、jpeg、tiff格式批量转webp格式

在Linux系统中&#xff0c;使用ImageMagick可以图片格式转换&#xff0c;其中最常用的是通过命令行工具进行。 ImageMagick是一个非常强大的图像处理工具集&#xff0c;它包含了许多用于图像转换的命令。 一、安装ImageMagick&#xff08;如果尚未安装&#xff09;&#xff1…

蓝牙资讯|iOS 18.1 正式版下周推送,AirPods Pro 2耳机将带来助听器功能

苹果公司宣布将在下周发布 iOS 18.1 正式版&#xff0c;同时确认该更新将为 AirPods Pro 2 耳机带来新增“临床级”助听器功能。在启用功能后&#xff0c;用户首先需要使用 AirPods 和 iPhone 进行简短的听力测试&#xff0c;如果检测到听力损失&#xff0c;系统将创建一项“个…

MoCoOp: Mixture of Prompt Learning for Vision Language Models

文章汇总 当前的问题 1)数据集风格变化。 如图1所示&#xff0c;对于一个数据集&#xff0c;单个软提示可能不足以捕获数据中呈现的各种样式。同一数据集中的不同实例可能与不同的提示符兼容。因此&#xff0c;更**自然的做法是使用多个提示来充分表示这些变化**。 2)过拟合…

101. UE5 GAS RPG 实现范围技能奥术爆发表现

在上一篇文章里&#xff0c;我们实现了范围技能的范围指示功能。范围指示是在释放技能前&#xff0c;确认技能的攻击范围&#xff0c;在确认位置后&#xff0c;通过额外按键进行触发技能释放。 在这一篇里&#xff0c;我们将先实现在技能里使用范围指示&#xff0c;并能够播放对…

硬件基础知识补全计划【一】电阻

一、电阻理论 1.1 电流定义 电流&#xff1a;电流的强弱用电流强度来描述&#xff0c;电流强度是单位时间内通过导体某一横截面的电荷量&#xff0c;简称电流&#xff0c;用I表示。1 秒内有 6.241509310^18 个元电荷通过横截面的电流&#xff0c;定义为 1 安 (A)。 电压&…

Redis Search系列 - 第四讲 支持中文

目录 一、支持中文二、自定义中文词典2.1 Redis Search设置FRISOINI参数2.2 friso.ini文件相关配置1&#xff09;自定义friso UTF-8字典2&#xff09;修改friso.ini配置文件 三、实测中文分词效果 一、支持中文 Redis Stack 从版本 0.99.0 开始支持中文文档的添加和分词。中文…

MoeCTF 2024 ---Misc方向WP

安全杂项 signin 题目描述&#xff1a; xdsec的小伙伴们和参赛者来上课&#xff0c;碰巧这一天签到系统坏了&#xff0c;作为老师的你&#xff0c;要帮他们 教师代签。 特殊提醒&#xff1a;luo同学今天好像在宿舍打游戏&#xff0c;不想来上课&#xff0c;这是严重的缺勤行为…

PoissonRecon学习笔记

1. Screened Poisson Reconstruction (SPR) 源码&#xff1a;https://github.com/mkazhdan/PoissonRecon However, as noted by several researchers, it suffers from a tendency to over-smooth the data. 泊松重建存在过度平滑的现象。 方法&#xff1a;position and gradi…

【QT】QChart绘制曲线与散点图

功能描述:绘制曲线和散点图,添加图例信息,可以进行缩放、移动,鼠标在曲线上时显示当前坐标点 QChart功能类 继承QGraphicsView 重写鼠标事件函数 protected:void resizeEvent(QResizeEvent *event);void mouseMoveEvent(QMouseEvent *event);void mousePressEvent(QMouseEv…

C++共同体

共同体是一种数据格式&#xff0c;他能储存不同的数据类型&#xff0c;但是同一时间只能储存其中的一种类型。 语法&#xff1a; union 共同体名 { 成员一的数据类型 成员名一&#xff1b; 成员二的数据类型 成员名二&#xff1b; 成员n的数据类型 成员名n&#xff1b; }

PHP养老院管理系统-计算机设计毕业源码-00115

摘要 随着社会老龄化进程的加速&#xff0c;养老院管理系统在提高养老服务质量和效率方面发挥着越来越重要的作用。本研究旨在设计和实现一个基于PHP的养老院管理系统&#xff0c;以满足养老院的日常管理需求&#xff0c;提升养老服务水平。 本研究首先对养老院管理系统的需求进…

大模型系列——幻觉

在kimi中输入提示词&#xff0c;得到本文脉络&#xff1a; 我想写大模型幻觉技术文章&#xff0c;请对以下标题进行补全和细化&#xff1a; 1、幻觉原因 2、幻觉消除方案 3、幻觉检测方案 4、幻觉评估数据集 背景 研究人员将大模型的幻觉分为事实性幻觉&#xff08;Factuali…

【状态机DP】力扣2786. 访问数组中的位置使分数最大

给你一个下标从 0 开始的整数数组 nums 和一个正整数 x 。 你 一开始 在数组的位置 0 处&#xff0c;你可以按照下述规则访问数组中的其他位置&#xff1a; 如果你当前在位置 i &#xff0c;那么你可以移动到满足 i < j 的 任意 位置 j 。 对于你访问的位置 i &#xff0c…

系统架构图设计(轻量级架构)

轻量级架构一般包括&#xff1a;表现层、业务逻辑层、持久层、数据库层 表现层架构 MVC 模型&#xff08;Model&#xff09;&#xff1a;应用程序的主体部分&#xff0c;表示业务数据和业务逻辑视图&#xff08;View&#xff09;&#xff1a;用户看到并与之交流的界面控制器&…

Windows 11优化利器:全方位定制你的操作系统

最近&#xff0c;有用户询问如何禁用Windows Defender&#xff0c;这让我想起了一款功能强大的Windows 11设置工具。这款工具不仅包含了禁用Defender的功能&#xff0c;还提供了许多其他实用的系统定制选项。 工具概览 这款名为“Windows11轻松设置”的软件&#xff0c;最近进…

延迟队列实现及其原理详解

1.绪论 本文主要讲解常见的几种延迟队列的实现方式&#xff0c;以及其原理。 2.延迟队列的使用场景 延迟队列主要用于解决每个被调度的任务开始执行的时间不一致的场景&#xff0c;主要包含如下场景: 1.比如订单超过15分钟后&#xff0c;关闭未关闭的订单。 2.比如用户可以…

保姆级教程来喽!从下载开始的Luatools~小白必看!

对于刚接触Luatools的新手朋友们&#xff0c;这篇保姆级教程将手把手教你如何从下载开始使用这款强大的调试工具。Luatools适用于合宙的多种4G模组&#xff0c;支持固件获取、打包、调试等多项功能&#xff0c;确保你的开发工作事半功倍。 本文就来讲解一下Luatools的下载和使…

Flask集成sqlalchemy (学习笔记)

文章目录 前言一、安装sqlalchemy二、连接mysql1.创建一个配置数据库信息的文件&#xff08;如上图&#xff09;2.创建sqlalchemy配置文件3.app.py中引入注册4.创建模型对象5.在app.py中进行关联6.执行映射语句&#xff08;迁移命令&#xff09; 总结 前言 本文章讲解的是分模…

Html/Vue浏览器下载并重命名文件

Html/Vue浏览器下载并重命名文件 row是上方图片的数据对象 download(row) {const link document.createElement(a);link.style.display none;// 设置下载地址link.setAttribute(href, row.url);// 设置文件名(这里可以重新设置名字&#xff0c;下载之后的文件就是你重新命名…

王源携手匡威,官宣全球代言人身份,引全网热议

近日&#xff0c;匡威隆重宣布&#xff0c;青年偶像王源荣膺其全球品牌代言人。在官宣消息发布前夕&#xff0c;王源与匡威的合作便已在微博热搜上占据头榜&#xff0c;备受广大网友关注。 随着官宣及产品上线的钟声敲响&#xff0c;王源的粉丝们迅速行动起来&#xff0c;积极支…