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

news2024/11/27 12:46:38

前言:

相信大家在网络不好的时候使用列表分页的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 = true
        let 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 in
                switch response.result {
                case .success(let catImages):
                    print(catImages)
                    catImagesArray.append(contentsOf: catImages)
                    isFetching = false
                    if(self.count >= 1){
                    //获取将要加载的区间,利用tableview.reloadRows加载的时候只加载这一区间即可,防止整体重新加载导致性能不佳
                        let statrIndex = catImagesArray.count - catImages.count
                        let endIndex = catImagesArray.count + catImages.count - 1
                        var 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 += 1
                case .failure(let error):
                    print("Error fetching data: \(error)")
                    isFetching = false
                }
            }
        }
    }
}

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

接着我定义了一些model:

//定义猫咪模型
struct CatImage : Codable {
    let url : String
    let 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 = .scaleAspectFill
        catImageView.layer.cornerRadius = 10
        catImageView.clipsToBounds = true
        catImageView.translatesAutoresizingMaskIntoConstraints = false
        self.contentView.addSubview(catImageView)
        
        // 配置名字标签
        nameLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium)
        nameLabel.textColor = .white
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        self.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.center
        loadingIndicator?.color = .white
        self.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 = nil
        nameLabel.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 in
                        DispatchQueue.main.async {
                            // 确保此时 indexPath 没有被重用
                            if catImagesArray[indexPath.row].url == catImage.url {
                                self?.catImageView.image = image
                                self?.loadingIndicator?.stopAnimating()
                            }
                        }
                    }
                }
            }
            
            // 设置名字标签
            if let breed = catImage.breeds.first {
                self.nameLabel.text = breed.name
            } else {
                self.nameLabel.text = ""
            }
        }
    }
}


extension CatTableViewCell{
    //URL 转 image
    func 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 in
            if 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 = 0
    lazy var tableView : UITableView = {
        let tableview = UITableView(frame: self.view.bounds)
        /...

        tableview.prefetchDataSource = self

        /...
        return tableview
    }()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .blue
        self.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.coloradmin.cn/o/2222805.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【案例演示】图像描述大模型示例及概念解释

【案例演示】图像描述大模型示例及概念解释 一、案例演示模型描述期望模型使用方式以及适用范围模型功能演示 二、大模型开源平台概览模型库的定义大模型开源平台 一、案例演示 模型链接&#xff1a;https://modelscope.cn/models/iic/mplug_image-captioning_coco_base_zh 模…

使用 CDN 后 Apache 的日志记录客户真实 IP

经常搭建网站服务器的都知道&#xff0c;在给站点使用了 CDN 后 Web 应用的日志记录里就会只记录 CDN 节点 IP 了&#xff0c;这就没法看到真实客户请求 IP&#xff0c;对于日志分析、运维日常维护来说就有点儿麻烦了&#xff0c;今天明月结合在五洛云服务器上搭建的Apache环境…

短视频账号矩阵系统源码---独立saas技术部署

#短视频账号矩阵系统# #短视频矩阵源码# #短视频账号矩阵系统技术开发# 抖音seo账号矩阵系统&#xff0c;短视频矩阵系统源码&#xff0c; 短视频矩阵是一种常见的视频编码标准&#xff0c;通过多账号一键授权管理的方式&#xff0c;为运营人员打造功能强大及全面的“矩阵式“…

liunx线程互斥

临界资源和临界区 临界资源&#xff1a;多线程执行流共享的资源就叫临界资源。 临界区&#xff1a;每个线程中&#xff0c;访问临界区的代码&#xff0c;就叫临界区。 互斥&#xff1a;任何时候&#xff0c;互斥保证只有一个执行流进入临界区&#xff0c;访问临界资源&#…

华为eNSP:端口安全

一、什么是端口安全 端口安全是指保护计算机端口免受未经授权的访问、攻击或滥用的一种措施。计算机上的每个服务或应用程序都依靠特定的端口进行通信。端口安全的目的是限制对计算机端口的访问&#xff0c;确保只有经过授权的用户或服务可以使用这些端口。通过配置防火墙、访…

C/C++(六)多态

本文将介绍C的另一个基于继承的重要且复杂的机制&#xff0c;多态。 一、多态的概念 多态&#xff0c;就是多种形态&#xff0c;通俗来说就是不同的对象去完成某个行为&#xff0c;会产生不同的状态。 多态严格意义上分为静态多态与动态多态&#xff0c;我们平常说的多态一般…

VulkanTutorial(1·环境搭建,渲染流程简述)

介绍&#xff1a; 与OpenGL&#xff0c;WebGL和Direct3D等API&#xff08;(Application Programming Interface, 应用程序编程接口)&#xff09;相比&#xff0c;valkan更偏向于底层&#xff0c;有更多的GPU控制接口&#xff0c;因此它有更好的性能和更小的驱动开销&#xff0…

【Python数据可视化】利用Matplotlib绘制美丽图表!

【Python数据可视化】利用Matplotlib绘制美丽图表&#xff01; 数据可视化是数据分析过程中的重要步骤&#xff0c;它能直观地展示数据的趋势、分布和相关性&#xff0c;帮助我们做出明智的决策。在 Python 中&#xff0c;Matplotlib 是最常用的可视化库之一&#xff0c;它功能…

【论文+源码】基于spring boot的垃圾分类网站

创建一个基于Spring Boot的垃圾分类网站涉及多个步骤&#xff0c;包括环境搭建、项目创建、数据库设计、后端服务开发、前端页面设计等。下面我将引导您完成这个过程。 第一步&#xff1a;准备环境 确保您的开发环境中安装了以下工具&#xff1a; Java JDK 8 或更高版本Mav…

python装饰器的另类用法

在对pyverilog源码进行单步调试时&#xff0c;遇到一个很奇怪的现象&#xff0c;被装饰器装饰的方法t_LINECOMMENT没有主动调用&#xff0c;但装饰器TOKEN中的内嵌函数set_regex却被调用了。 ## lexer.pyfrom ply.lex import *class VerilogLexer(object):linecomment r"…

C++【string类的使用】(上)

文章目录 1. 为什么要学习string类2. 标准库的string类2.1 string的构造函数&#xff08;1&#xff09;无参构造&#xff08;重点&#xff09;&#xff08;2&#xff09;用字符串初始化&#xff08;重点&#xff09;&#xff08;3&#xff09;用字符串的前n个字符初始化(4)拷贝…

常见ElasticSearch 面试题解析(上)

前言 ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎&#xff0c;基于RESTful web接口。Elasticsearch是用Java语言开发的&#xff0c;并作为Apache许可条款下的开放源码发布&#xff0c;是一种流行的企业级搜索引擎。ElasticSearch…

直播间“饕餮盛宴”的背后,是“他经济”正在冒头

最近&#xff0c;一个有意思的现象逐渐露出了苗头。 今年“双11”第一轮尾款开启支付的当晚&#xff0c;罗永浩的直播间上演了一出别样的“饕餮盛宴”。直播开场后&#xff0c;iphone16系列、可口可乐&#xff08;含糖、无糖300ml&#xff09;10秒售罄&#xff0c;索尼PS5、沃…

hive on tez 指定队列后任务一直处于running状态

如上图所示一直处于running状态&#xff0c;查看日志发现一直重复弹出同一个info&#xff1a; 2024-10-18 16:57:32,739 [INFO] [AMRM Callback Handler Thread] |rm.YarnTaskSchedulerService|: Allocated: <memory:0, vCores:0> 释义: 当前应用程序没有分配到任何内存…

wordcloud 字体报错

wordcloud 字体报错 词云库报错&#xff1a;Only supported for TrueType fonts字体文件问题pillow版本的问题wordcloud版本问题&#xff08;我的最终解决方案&#xff09; 词云库报错&#xff1a;Only supported for TrueType fonts 字体文件问题 解决方法 写绝对路径 &…

教程分享!超简单的低功耗4G模组LCD应用示例!看过来~

低功耗4G模组LCD应用是物联网技术中的一部分知识&#xff0c;在未来的学习和实践中&#xff0c;我们还将接触到更多前沿的技术和理念。让我们一起努力&#xff0c;探索科技的无限可能&#xff0c;为我们的生活带来更多便利与惊喜&#xff01;希望本文能为您提供一些帮助&#x…

Cisco WLC 9800 - HA SSO with Ether-channel

本文将记录如何配置HA SSO以及Ethernet Channel。 1.拓扑情况 本文的内容基于如下的Topo进行。 2.准备工作 两台WLC的型号必须一样&#xff1b;两台WLC必须使用一样的软件版本&#xff1b;需要准备好使用的IP地址&#xff1a;两个用于WLC的管理地址&#xff08;WMI&#xff…

串口通讯编程示例之串口编写程序

使用open()函数打开串口设备 首先使用open()函数打开串口设备/dev/ttymxc6&#xff0c;设备使用了O_RDWR | O_NOCTTY | O_NDELAY标志&#xff0c;分别代表以读写方式打开、不让设备成为控制终端且设置非阻塞模式&#xff0c;也就是当无法打开设备时&#xff0c;不会在原地等待&…

【博客节选】Unity角色异常抖动问题排查

本文截取自本人文章 &#xff1a;【Unity实战笔记】第二一 基于状态模式的角色控制——以UnityChan为例 发现出现角色抖动问题 尝试解决方法&#xff1a; 跳跃的loop time不要勾选&#xff1b; 相机aim添加垂直阻尼 还是不行&#xff0c;仔细查看是位移时震颤。 UnityCha…

HCIP-HarmonyOS Application Developer 习题(十三)

&#xff08;多选&#xff09;1、在设计应用框架的过程中&#xff0c;我们常用的界面应用框架有哪些? A、启动页 B、详情页 C、列表视图 D、网格视图 答案&#xff1a;ABCD 分析&#xff1a; &#xff08;多选&#xff09;2、触摸屏以触控的方式进行输入。它可以支持以下哪些…