无限滚动UITableView与数字



我需要创建带有数字的无限表格,即在滚动时,应该创建带有数字的新单元格。

我创建APICaller与计数器,分页,数组和while循环。我还创建了UITableViewfunc scrollViewDidScroll(_ scrollView: UIScrollView),在表中附加新的值。

MyViewControllerwithUITableView

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate {

private let apiCaller = APICaller()

private let tableView: UITableView  = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.register(UITableViewCell.self,
forCellReuseIdentifier: "cell")
return tableView
}()

private var data = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.dataSource = self
tableView.delegate = self
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame  = view.bounds
apiCaller.fetchData(pagination: false, completion: { [weak self] result in
switch result {
case.success(let data):
self?.data.append(contentsOf: data)
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case.failure(_):
break
}
})
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text =  String(describing: data[indexPath.row])
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let position = scrollView.contentOffset.y
if position > (tableView.contentSize.height-100-scrollView.frame.size.height) {
guard !apiCaller.isPaginating else { return }
apiCaller.fetchData(pagination: true) { [weak self] result in
switch result {
case .success(let moreData):
self?.data.append(contentsOf: moreData)
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case .failure(_):
break
}
}
}
}
}

APICaller的这种情况下,我只有100个单元格,这对应于循环约束(但如果我从while循环中删除break,则没有出现任何内容)

class APICaller {

private var counter = 0

var isPaginating = false

func fetchData(pagination: Bool = false, completion: @escaping (Result<[Int], Error>) -> Void) {
if pagination {
isPaginating = true
}
DispatchQueue.global().asyncAfter(deadline: .now() + (pagination ? 3 : 2), execute: {
var newData: [Int] = [0]
var originalData: [Int] = [0]

while true {
self.counter += 1
originalData.append(self.counter)
if  self.counter == 100 {
break
}
}

completion(.success(pagination ? newData : originalData))
if pagination {
self.isPaginating = false
}
})
}
}

那么,我怎么能得到一个无限的数字表?

基本思想是正确的。当您滚动到需要更多行时,获取它们。但是在UIScrollViewDelegate中这样做是一个昂贵的地方。也就是说,该方法对每个移动像素调用,并将导致许多冗余调用。

我个人建议将此逻辑移动到适当的表视图方法。例如,最低限度,您可以在UITableViewDataSource方法中做到这一点(即,如果您正在处理来自数据集末尾的n行,则获取更多数据)。例如,

extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count + 1                     // NB: one extra for the final “busy” cell
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath.row + 50) >= data.count {   // scrolled within 50 rows of end
fetch()
}
if indexPath.row >= data.count {          // if at last row, show spinner
return tableView.dequeueReusableCell(withIdentifier: "busy", for: indexPath)
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = String(describing: data[indexPath.row])
return cell
}
}

有几点需要注意:

  • 我要多报告一行。这是我的"忙碌"单元(一个具有旋转UIActivityIndicatorView的单元)。这样,如果用户滚动的速度超过了网络响应的承受能力,我们至少会向用户显示一个旋转器,让他们知道我们正在获取更多的数据。

  • 因此,cellForRowAt检查row,并在必要时显示"忙cell"。

  • 在这种情况下,当我在结束的50项以内时,我将启动下一批数据的获取。

比上面更好的是,我还会将UITableViewDataSource实现与UITableViewDataSourcePrefetching结合起来。因此,设置tableView.prefetchDataSource,然后实现prefetchRowsAt:

extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
guard
let maxIndexPath = indexPaths.max(by: { $0.row < $1.row }),  // get the last row, and
maxIndexPath.row >= data.count                               // see if it exceeds what we have already fetched
else { return }
fetch(pagination: true)
}
}

顺便说一下,关于fetch的一些注意事项:

func fetch(pagination: Bool = false) {
apiCaller.fetchData(pagination: pagination) { [weak self] result in
guard
let self = self,
case .success(let values) = result
else { return }
let oldCount = self.data.count
self.data.append(contentsOf: values)
let indexPaths = (oldCount ..< self.data.count).map { IndexPath(row: $0, section: 0) }
self.tableView.insertRows(at: indexPaths, with: .automatic)
}
}

注意,我建议不要"重新加载"整个表视图,而只是"插入"适当的行。我还将"我很忙"逻辑移动到APICaller中,它属于:

class APICaller {
private var counter = 0
private var isInProgress = false
func fetchData(pagination: Bool = false, completion: @escaping (Result<[Int], Error>) -> Void) {
guard !isInProgress else {
completion(.failure(APIError.busy))
return
}
isInProgress = true
DispatchQueue.main.asyncAfter(deadline: .now() + (pagination ? 3 : 2)) { [weak self] in
guard let self = self else { return }
let oldCounter = self.counter
self.counter += 100
self.isInProgress = false
let values = Array(oldCounter ..< self.counter)
completion(.success(values))
}
}
}
extension APICaller {
enum APIError: Error {
case busy
}
}

我不仅简化了APICaller,而且使其线程安全(通过将所有状态突变和回调移动到主队列上)。如果您在后台队列上启动一些异步任务,请将更新和回调分派到主队列。但是不要从后台线程改变对象(或者如果你必须,添加一些同步逻辑)。