Как узнать, когда UITableView завершил ReloadData?
Я пытаюсь прокрутить до конца UITableView после того, как это сделано, выполняя [self.tableView reloadData]
У меня изначально был
[self.tableView reloadData]
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Но затем я прочитал, что reloadData является асинхронным, поэтому прокрутка не происходит, так как self.tableView
, [self.tableView numberOfSections]
а также [self.tableView numberOfRowsinSection
все 0.
Спасибо!
Что странно, что я использую:
[self.tableView reloadData];
NSLog(@"Number of Sections %d", [self.tableView numberOfSections]);
NSLog(@"Number of Rows %d", [self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1);
В консоли возвращает Sections = 1, Row = -1;
Когда я делаю точно такие же NSLogs в cellForRowAtIndexPath
Я получаю Sections = 1 и Row = 8; (8 верно)
21 ответ
Перезагрузка происходит во время следующего прохода макета, что обычно происходит, когда вы возвращаете управление в цикл выполнения (например, после нажатия кнопки или любого другого возврата).
Таким образом, один из способов запустить что-то после перезагрузки табличного представления - просто заставить табличное представление немедленно выполнить компоновку:
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Другой способ - запланировать запуск кода после компоновки, используя dispatch_async
:
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection:([self.tableView numberOfSections]-1)];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
});
ОБНОВИТЬ
После дальнейшего расследования я обнаружил, что табличное представление отправляет tableView:numberOfSections:
а также tableView:numberOfRowsInSection:
к источнику данных, прежде чем вернуться из reloadData
, Если делегат реализует tableView:heightForRowAtIndexPath:
, табличное представление также отправляет это (для каждой строки) перед возвратом из reloadData
,
Тем не менее, представление таблицы не отправляет tableView:cellForRowAtIndexPath:
или же tableView:headerViewForSection
до фазы макета, которая происходит по умолчанию, когда вы возвращаете управление в цикл выполнения.
Я также обнаружил, что в крошечной тестовой программе код в вашем вопросе правильно прокручивается до нижней части табличного представления, и я ничего особенного не делал (например, отправка layoutIfNeeded
или используя dispatch_async
).
Swift:
extension UITableView {
func reloadData(completion: ()->()) {
UIView.animateWithDuration(0, animations: { self.reloadData() })
{ _ in completion() }
}
}
...somewhere later...
tableView.reloadData {
println("done")
}
Objective-C:
[UIView animateWithDuration:0 animations:^{
[myTableView reloadData];
} completion:^(BOOL finished) {
//Do something after that...
}];
Начиная с Xcode 8.2.1, iOS 10 и swift 3,
Вы можете определить конец tableView.reloadData()
легко с помощью блока CATransaction:
CATransaction.begin()
CATransaction.setCompletionBlock({
print("reload completed")
//Your completion code here
)}
print("reloading")
tableView.reloadData()
CATransaction.commit()
Вышеприведенное также работает для определения конца reloadData () UICollectionView и reloadAllComponents UIPickerView.
dispatch_async(dispatch_get_main_queue())
выше метод не гарантированно работает. Я вижу недетерминированное поведение с этим, в котором иногда система завершает layoutSubviews и рендеринг ячейки до блока завершения, а иногда и после.
Вот решение, которое работает на 100% для меня, на iOS 10. Требуется возможность создания экземпляра UITableView или UICollectionView в качестве пользовательского подкласса. Вот решение UICollectionView, но оно точно такое же для UITableView:
CustomCollectionView.h:
#import <UIKit/UIKit.h>
@interface CustomCollectionView: UICollectionView
- (void)reloadDataWithCompletion:(void (^)(void))completionBlock;
@end
CustomCollectionView.m:
#import "CustomCollectionView.h"
@interface CustomCollectionView ()
@property (nonatomic, copy) void (^reloadDataCompletionBlock)(void);
@end
@implementation CustomCollectionView
- (void)reloadDataWithCompletion:(void (^)(void))completionBlock
{
self.reloadDataCompletionBlock = completionBlock;
[super reloadData];
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.reloadDataCompletionBlock) {
self.reloadDataCompletionBlock();
self.reloadDataCompletionBlock = nil;
}
}
@end
Пример использования:
[self.collectionView reloadDataWithCompletion:^{
// reloadData is guaranteed to have completed
}];
Смотрите здесь для Swift версию этого ответа
У меня были те же проблемы, что и у Тайлера Шиффера.
Я реализовал его решение в Swift, и оно решило мои проблемы.
Swift 3.0:
final class UITableViewWithReloadCompletion: UITableView {
private var reloadDataCompletionBlock: (() -> Void)?
override func layoutSubviews() {
super.layoutSubviews()
reloadDataCompletionBlock?()
reloadDataCompletionBlock = nil
}
func reloadDataWithCompletion(completion: @escaping () -> Void) {
reloadDataCompletionBlock = completion
super.reloadData()
}
}
Свифт 2:
class UITableViewWithReloadCompletion: UITableView {
var reloadDataCompletionBlock: (() -> Void)?
override func layoutSubviews() {
super.layoutSubviews()
self.reloadDataCompletionBlock?()
self.reloadDataCompletionBlock = nil
}
func reloadDataWithCompletion(completion:() -> Void) {
reloadDataCompletionBlock = completion
super.reloadData()
}
}
Пример использования:
tableView.reloadDataWithCompletion() {
// reloadData is guaranteed to have completed
}
И UICollectionView
версия, основанная на ответе kolaworld:
/questions/15054634/kak-uznat-kogda-uitableview-zavershil-reloaddata/15054642#15054642
Необходимо тестирование. Пока работает на iOS 9.2, Xcode 9.2 beta 2, с прокруткой collectionView к индексу, как закрытие.
extension UICollectionView
{
/// Calls reloadsData() on self, and ensures that the given closure is
/// called after reloadData() has been completed.
///
/// Discussion: reloadData() appears to be asynchronous. i.e. the
/// reloading actually happens during the next layout pass. So, doing
/// things like scrolling the collectionView immediately after a
/// call to reloadData() can cause trouble.
///
/// This method uses CATransaction to schedule the closure.
func reloadDataThenPerform(_ closure: @escaping (() -> Void))
{
CATransaction.begin()
CATransaction.setCompletionBlock(closure)
self.reloadData()
CATransaction.commit()
}
}
Использование:
myCollectionView.reloadDataThenPerform {
myCollectionView.scrollToItem(at: indexPath,
at: .centeredVertically,
animated: true)
}
Кажется, что люди все еще читают этот вопрос и ответы. В связи с этим, я редактирую свой ответ, чтобы удалить слово Синхронный, которое на самом деле не имеет к этому отношения.
When [tableView reloadData]
возвращает, внутренние структуры данных за tableView были обновлены. Поэтому, когда метод завершится, вы можете безопасно перейти к нижней части. Я подтвердил это в моем собственном приложении. Широко принятый ответ @ rob-mayoff, хотя и запутанный в терминологии, подтверждает то же самое в своем последнем обновлении.
Если твой tableView
не прокручивая до конца, у вас может быть проблема с другим кодом, который вы не опубликовали. Возможно, вы меняете данные после завершения прокрутки и не перезагружаете и / или не прокручиваете их до конца?
Добавьте некоторые записи, как показано ниже, чтобы убедиться, что данные таблицы верны после reloadData
, У меня есть следующий код в примере приложения, и он отлично работает.
// change the data source
NSLog(@"Before reload / sections = %d, last row = %d",
[self.tableView numberOfSections],
[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);
[self.tableView reloadData];
NSLog(@"After reload / sections = %d, last row = %d",
[self.tableView numberOfSections],
[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]-1
inSection:[self.tableView numberOfSections] - 1]
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
Я использую этот трюк, уверен, я уже разместил его в дубликате этого вопроса:
-(void)tableViewDidLoadRows:(UITableView *)tableView{
// do something after loading, e.g. select a cell.
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// trick to detect when table view has finished loading.
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];
[self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];
// specific to your controller
return self.objects.count;
}
На самом деле этот решил мою проблему:
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSSet *visibleSections = [NSSet setWithArray:[[tableView indexPathsForVisibleRows] valueForKey:@"section"]];
if (visibleSections) {
// hide the activityIndicator/Loader
}}
В Swift 3.0 + мы можем создать расширение для UITableView
с escaped Closure
как ниже:
extension UITableView {
func reloadData(completion: @escaping () -> ()) {
UIView.animate(withDuration: 0, animations: { self.reloadData()})
{_ in completion() }
}
}
И используйте его как ниже, где вы хотите:
Your_Table_View.reloadData {
print("reload done")
}
надеюсь, это кому-нибудь поможет. ура!
Попробуй так будет работать
[tblViewTerms performSelectorOnMainThread:@selector(dataLoadDoneWithLastTermIndex:) withObject:lastTermIndex waitUntilDone:YES];waitUntilDone:YES];
@interface UITableView (TableViewCompletion)
-(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex;
@end
@implementation UITableView(TableViewCompletion)
-(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex
{
NSLog(@"dataLoadDone");
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: [lastTermIndex integerValue] inSection: 0];
[self selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
}
@end
Я выполню, когда таблица полностью загружена
Другое решение - вы можете создать подкласс UITableView
Попробуй это:
tableView.backgroundColor =.black
tableView.reloadData()
DispatchQueue.main.async (execute: {
tableView.backgroundColor = .green
})
// Цвет tableView изменится с черного на зеленый только после завершения функции reloadData().
Детали
- Xcode версии 10.2.1 (10E1001), Swift 5
Решение
import UIKit
// MARK: - UITableView reloading functions
protocol ReloadCompletable: class { func reloadData() }
extension ReloadCompletable {
func run(transaction closure: (() -> Void)?, completion: (() -> Void)?) {
guard let closure = closure else { return }
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
closure()
CATransaction.commit()
}
func run(transaction closure: (() -> Void)?, completion: ((Self) -> Void)?) {
run(transaction: closure) { [weak self] in
guard let self = self else { return }
completion?(self)
}
}
func reloadData(completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadData() }, completion: closure)
}
}
// MARK: - UITableView reloading functions
extension ReloadCompletable where Self: UITableView {
func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation, completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadRows(at: indexPaths, with: animation) }, completion: closure)
}
func reloadSections(_ sections: IndexSet, with animation: UITableView.RowAnimation, completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadSections(sections, with: animation) }, completion: closure)
}
}
// MARK: - UICollectionView reloading functions
extension ReloadCompletable where Self: UICollectionView {
func reloadSections(_ sections: IndexSet, completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadSections(sections) }, completion: closure)
}
func reloadItems(at indexPaths: [IndexPath], completion closure: ((Self) -> Void)?) {
run(transaction: { [weak self] in self?.reloadItems(at: indexPaths) }, completion: closure)
}
}
Применение
UITableView
// Activate
extension UITableView: ReloadCompletable { }
// ......
let tableView = UICollectionView()
// reload data
tableView.reloadData { tableView in print(collectionView) }
// or
tableView.reloadRows(at: indexPathsToReload, with: rowAnimation) { tableView in print(tableView) }
// or
tableView.reloadSections(IndexSet(integer: 0), with: rowAnimation) { _tableView in print(tableView) }
UICollectionView
// Activate
extension UICollectionView: ReloadCompletable { }
// ......
let collectionView = UICollectionView()
// reload data
collectionView.reloadData { collectionView in print(collectionView) }
// or
collectionView.reloadItems(at: indexPathsToReload) { collectionView in print(collectionView) }
// or
collectionView.reloadSections(IndexSet(integer: 0)) { collectionView in print(collectionView) }
Полный образец
Не забудьте добавить сюда код решения
import UIKit
class ViewController: UIViewController {
private weak var navigationBar: UINavigationBar?
private weak var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationItem()
setupTableView()
}
}
// MARK: - Activate UITableView reloadData with completion functions
extension UITableView: ReloadCompletable { }
// MARK: - Setup(init) subviews
extension ViewController {
private func setupTableView() {
guard let navigationBar = navigationBar else { return }
let tableView = UITableView()
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.dataSource = self
self.tableView = tableView
}
private func setupNavigationItem() {
let navigationBar = UINavigationBar()
view.addSubview(navigationBar)
self.navigationBar = navigationBar
navigationBar.translatesAutoresizingMaskIntoConstraints = false
navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
let navigationItem = UINavigationItem()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "all", style: .plain, target: self, action: #selector(reloadAllCellsButtonTouchedUpInside(source:)))
let buttons: [UIBarButtonItem] = [
.init(title: "row", style: .plain, target: self,
action: #selector(reloadRowButtonTouchedUpInside(source:))),
.init(title: "section", style: .plain, target: self,
action: #selector(reloadSectionButtonTouchedUpInside(source:)))
]
navigationItem.leftBarButtonItems = buttons
navigationBar.items = [navigationItem]
}
}
// MARK: - Buttons actions
extension ViewController {
@objc func reloadAllCellsButtonTouchedUpInside(source: UIBarButtonItem) {
let elementsName = "Data"
print("-- Reloading \(elementsName) started")
tableView?.reloadData { taleView in
print("-- Reloading \(elementsName) stopped \(taleView)")
}
}
private var randomRowAnimation: UITableView.RowAnimation {
return UITableView.RowAnimation(rawValue: (0...6).randomElement() ?? 0) ?? UITableView.RowAnimation.automatic
}
@objc func reloadRowButtonTouchedUpInside(source: UIBarButtonItem) {
guard let tableView = tableView else { return }
let elementsName = "Rows"
print("-- Reloading \(elementsName) started")
let indexPathToReload = tableView.indexPathsForVisibleRows?.randomElement() ?? IndexPath(row: 0, section: 0)
tableView.reloadRows(at: [indexPathToReload], with: randomRowAnimation) { _tableView in
//print("-- \(taleView)")
print("-- Reloading \(elementsName) stopped in \(_tableView)")
}
}
@objc func reloadSectionButtonTouchedUpInside(source: UIBarButtonItem) {
guard let tableView = tableView else { return }
let elementsName = "Sections"
print("-- Reloading \(elementsName) started")
tableView.reloadSections(IndexSet(integer: 0), with: randomRowAnimation) { _tableView in
//print("-- \(taleView)")
print("-- Reloading \(elementsName) stopped in \(_tableView)")
}
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int { return 1 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 20 }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(Date())"
return cell
}
}
Полученные результаты
https://stackru.com/image s/3da3fdcb99e2134b55cf4e89503d6cc73b235f1b.png
В итоге я использовал вариант решения Шона:
Создайте пользовательский класс UITableView с делегатом:
protocol CustomTableViewDelegate {
func CustomTableViewDidLayoutSubviews()
}
class CustomTableView: UITableView {
var customDelegate: CustomTableViewDelegate?
override func layoutSubviews() {
super.layoutSubviews()
self.customDelegate?.CustomTableViewDidLayoutSubviews()
}
}
Затем в моем коде я использую
class SomeClass: UIViewController, CustomTableViewDelegate {
@IBOutlet weak var myTableView: CustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
self.myTableView.customDelegate = self
}
func CustomTableViewDidLayoutSubviews() {
print("didlayoutsubviews")
// DO other cool things here!!
}
}
Также убедитесь, что вы установили табличное представление на CustomTableView в конструкторе интерфейса:
Если вы перезагрузите данные, когда
viewDidLoad
, вы можете поместить свой код в метод. Но вы должны быть осторожны, чтобы
viewDidLayoutSubviews
может быть вызван несколько раз.
Я регулярно сталкивался с одной и той же проблемой, и это происходило даже с несколькими строками в таблице (и в итоге я добавил некоторые надуманные обходные пути, о которых упоминали другие).
Однако последняя заметка в ответе Роба (в которой говорится, что опубликованный код у него действительно работает нормально) заставила меня задуматься. Я заметил, что в моем конкретном случае проблема возникла только тогда, когда вызов перезагрузки был выполнен внутри метода.
Решением было просто обернуть всю логику вdispatch_async(dispatch_get_main_queue(), ^{...})
внутриobserveValueForKeyPath()
метод.
Примечание. Я не тестировал это решение на очень больших наборах данных.
Просто чтобы предложить другой подход, основанный на идее завершения, являющейся "последней видимой" ячейкой, которую нужно отправить cellForRow
,
// Will be set when reload is called
var lastIndexPathToDisplay: IndexPath?
typealias ReloadCompletion = ()->Void
var reloadCompletion: ReloadCompletion?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Setup cell
if indexPath == self.lastIndexPathToDisplay {
self.lastIndexPathToDisplay = nil
self.reloadCompletion?()
self.reloadCompletion = nil
}
// Return cell
...
func reloadData(completion: @escaping ReloadCompletion) {
self.reloadCompletion = completion
self.mainTable.reloadData()
self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last
}
Одна из возможных проблем: если reloadData()
закончил до lastIndexPathToDisplay
была установлена, последняя видимая ячейка будет отображаться раньше lastIndexPathToDisplay
был установлен, и завершение не будет вызвано (и будет в состоянии ожидания):
self.mainTable.reloadData()
// cellForRowAt could be finished here, before setting `lastIndexPathToDisplay`
self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last
Если мы повернем вспять, мы можем закончить с завершением при прокрутке до reloadData()
,
self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last
// cellForRowAt could trigger the completion by scrolling here since we arm 'lastIndexPathToDisplay' before 'reloadData()'
self.mainTable.reloadData()
Вы можете использовать функцию performBatchUpdates из uitableview
Вот как можно добиться
self.tableView.performBatchUpdates({
//Perform reload
self.tableView.reloadData()
}) { (completed) in
//Reload Completed Use your code here
}
Создание многоразового расширения CATransaction:
public extension CATransaction {
static func perform(method: () -> Void, completion: @escaping () -> Void) {
begin()
setCompletionBlock {
completion()
}
method()
commit()
}
}
Теперь создаем расширение UITableView, которое будет использовать метод расширения CATransaction:
public extension UITableView {
func reloadData(completion: @escaping (() -> Void)) {
CATransaction.perform(method: {
reloadData()
}, completion: completion)
}
}
Применение:
tableView.reloadData(completion: {
//Do the stuff
})
Вы можете использовать его для выполнения каких-либо действий после перезагрузки данных:
[UIView animateWithDuration:0 animations:^{
[self.contentTableView reloadData];
} completion:^(BOOL finished) {
_isUnderwritingUpdate = NO;
}];
Попробуйте установить задержки:
[_tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0.2];
[_activityIndicator performSelector:@selector(stopAnimating) withObject:nil afterDelay:0.2];