我需要创建带有数字的无限表格,即在滚动时,应该创建带有数字的新单元格。
我创建APICaller
与计数器,分页,数组和while循环。我还创建了UITableView
和func scrollViewDidScroll(_ scrollView: UIScrollView)
,在表中附加新的值。
MyViewController
withUITableView
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
,而且使其线程安全(通过将所有状态突变和回调移动到主队列上)。如果您在后台队列上启动一些异步任务,请将更新和回调分派到主队列。但是不要从后台线程改变对象(或者如果你必须,添加一些同步逻辑)。