Wie kann man feststellen, wann UITableView ReloadData abgeschlossen hat?

183

Ich versuche, nach Abschluss der Ausführung zum Ende einer UITableView zu scrollen [self.tableView reloadData]

Ich hatte ursprünglich

 [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];

Aber dann habe ich gelesen, dass reloadData asynchron ist, so dass das Scrollen seit dem nicht mehr stattfindet self.tableView , [self.tableView numberOfSections]und [self.tableView numberOfRowsinSectionsind alle 0.

Vielen Dank!

Was seltsam ist, dass ich benutze:

[self.tableView reloadData];
NSLog(@"Number of Sections %d", [self.tableView numberOfSections]);
NSLog(@"Number of Rows %d", [self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1);

In der Konsole wird Sections = 1, Row = -1 zurückgegeben.

Wenn ich genau die gleichen NSLogs in mache, erhalte cellForRowAtIndexPathich Sections = 1 und Row = 8; (8 ist richtig)

Alan
quelle
Mögliches Duplikat dieser Frage: stackoverflow.com/questions/4163579/…
pmk
2
beste Lösung, die ich gesehen habe. stackoverflow.com/questions/1483581/…
Khaled Annajar
Meine Antwort für das Folgende könnte Ihnen helfen, stackoverflow.com/questions/4163579/…
Suhas Aithal
Versuchen Sie meine Antwort hier - stackoverflow.com/questions/4163579/…
Suhas Aithal

Antworten:

288

Das Neuladen erfolgt während des nächsten Layoutdurchlaufs, der normalerweise erfolgt, wenn Sie die Steuerung an die Ausführungsschleife zurückgeben (z. B. nach Ihrer Tastenaktion oder was auch immer zurückkehrt).

Eine Möglichkeit, nach dem erneuten Laden der Tabellenansicht etwas auszuführen, besteht darin, die Tabellenansicht zu zwingen, das Layout sofort auszuführen:

[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];

Eine andere Möglichkeit besteht darin, den Code nach dem Layout so zu planen, dass er später ausgeführt wird 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];
});

AKTUALISIEREN

Bei weiteren Untersuchungen stelle ich fest, dass die Tabellenansicht vor der Rückkehr von tableView:numberOfSections:und tableView:numberOfRowsInSection:an ihre Datenquelle gesendet wird reloadData. Wenn der Delegat implementiert tableView:heightForRowAtIndexPath:, sendet die Tabellenansicht dies auch (für jede Zeile), bevor Sie von zurückkehren reloadData.

Die Tabellenansicht sendet jedoch nicht tableView:cellForRowAtIndexPath:oder tableView:headerViewForSectionerst in der Layoutphase. Dies geschieht standardmäßig, wenn Sie die Steuerung an die Ausführungsschleife zurückgeben.

Ich finde auch, dass in einem winzigen Testprogramm der Code in Ihrer Frage ordnungsgemäß zum Ende der Tabellenansicht gescrollt wird, ohne dass ich etwas Besonderes tue (wie Senden layoutIfNeededoder Verwenden dispatch_async).

Rob Mayoff
quelle
3
@rob, abhängig davon, wie groß Ihre Tabellendatenquelle ist, können Sie animieren, in derselben Ausführungsschleife zum Ende der Tabellenansicht zu gelangen. Wenn Sie Ihren Testcode mit einer großen Tabelle ausprobieren, funktioniert Ihr Trick, GCD zu verwenden, um das Scrollen zu verzögern, bis die nächste Ausführungsschleife funktioniert, während das sofortige Scrollen fehlschlägt. Aber trotzdem, danke für diesen Trick !!
Herr T
7
Methode 2 hat aus unbekannten Gründen bei mir nicht funktioniert, sondern stattdessen die erste Methode gewählt.
Raj Pawan Gumdal
4
Es dispatch_async(dispatch_get_main_queue())ist nicht garantiert, dass die Methode funktioniert. Ich sehe damit ein nicht deterministisches Verhalten, bei dem das System manchmal die layoutSubviews und das Rendern von Zellen vor dem Abschlussblock und manchmal danach abgeschlossen hat. Ich werde unten eine Antwort posten, die für mich funktioniert hat.
Tyler Sheaffer
3
Stimmen Sie zu, dispatch_async(dispatch_get_main_queue())nicht immer zu arbeiten. Hier werden zufällige Ergebnisse angezeigt.
Vojto
1
Der Haupt-Thread läuft ein NSRunLoop. Eine Run-Schleife hat verschiedene Phasen, und Sie können einen Rückruf für eine bestimmte Phase planen (mit a CFRunLoopObserver). UIKit plant das Layout für eine spätere Phase, nachdem Ihr Event-Handler zurückgekehrt ist.
Rob Mayoff
106

Schnell:

extension UITableView {
    func reloadData(completion:@escaping ()->()) {
        UIView.animateWithDuration(0, animations: { self.reloadData() })
            { _ in completion() }
    }
}

...somewhere later...

tableView.reloadData {
    println("done")
}

Ziel c:

[UIView animateWithDuration:0 animations:^{
    [myTableView reloadData];
} completion:^(BOOL finished) {
    //Do something after that...
}];
Aviel Gross
quelle
16
Das ist gleichbedeutend damit, in "naher Zukunft" etwas auf dem Haupt-Thread zu versenden. Es ist wahrscheinlich, dass Sie nur sehen, wie die Tabellenansicht die Objekte rendert, bevor der Hauptthread den Abschlussblock entkoppelt. Es wird nicht empfohlen, diese Art von Hack überhaupt durchzuführen, aber in jedem Fall sollten Sie dispatch_after verwenden, wenn Sie dies versuchen möchten.
SEO
1
Die Rob-Lösung ist gut, funktioniert aber nicht, wenn die Tabellenansicht keine Zeilen enthält. Die Lösung von Aviel hat den Vorteil, dass sie auch dann funktioniert, wenn die Tabelle keine Zeilen, sondern nur Abschnitte enthält.
Chrstph SLN
@Christophe Ab sofort konnte ich Robs Update in einer Tabellenansicht ohne Zeilen verwenden, indem ich in meinem Mock View Controller die tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> IntMethode überschrieb und in meine Überschreibung alles einfügte, was ich benachrichtigen wollte, dass das Neuladen abgeschlossen war.
Gobe
49

Ab Xcode 8.2.1, iOS 10 und Swift 3

Sie können das Ende tableView.reloadData()einfach mithilfe eines CATransaction-Blocks bestimmen:

CATransaction.begin()
CATransaction.setCompletionBlock({
    print("reload completed")
    //Your completion code here
})
print("reloading")
tableView.reloadData()
CATransaction.commit()

Das Obige funktioniert auch zum Bestimmen des Endes von reloadData () von UICollectionView und reloadAllComponents () von UIPickerView.

kolaworld
quelle
👍 Ich arbeite auch, wenn Sie ein benutzerdefiniertes Neuladen durchführen, z. B. das manuelle Einfügen, Löschen oder Verschieben von Zeilen in der Tabellenansicht innerhalb von beginUpdatesund endUpdatesAufrufe.
Darrarski
Ich glaube, das ist eigentlich die moderne Lösung. in der Tat ist es das übliche Muster in iOS, Beispiel ... stackoverflow.com/a/47536770/294884
Fattie
Ich habe es versucht. Ich habe ein sehr merkwürdiges Verhalten. Meine Tabellenansicht zeigt zwei HeaderViews korrekt an. In setCompletionBlockmeinen numberOfSectionsShows 2 ... soweit so gut. Doch wenn setCompletionBlockich drinnen bin tableView.headerView(forSection: 1), kehrt es zurück nil!!! Daher denke ich, dass dieser Block entweder vor dem Neuladen auftritt oder etwas zuvor erfasst oder ich etwas falsch mache. Zu Ihrer Information, ich habe Tylers Antwort ausprobiert und das hat funktioniert! @ Fattie
Honey
32

Es dispatch_async(dispatch_get_main_queue())ist nicht garantiert, dass die oben beschriebene Methode funktioniert . Ich sehe damit ein nicht deterministisches Verhalten, bei dem das System manchmal die layoutSubviews und das Rendern von Zellen vor dem Abschlussblock und manchmal danach abgeschlossen hat.

Hier ist eine Lösung, die unter iOS 10 zu 100% für mich funktioniert. Sie erfordert die Fähigkeit, UITableView oder UICollectionView als benutzerdefinierte Unterklasse zu instanziieren. Hier ist die UICollectionView-Lösung, aber für UITableView ist es genau dasselbe:

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;
    [self reloadData];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (self.reloadDataCompletionBlock) {
        self.reloadDataCompletionBlock();
        self.reloadDataCompletionBlock = nil;
    }
}

@end

Anwendungsbeispiel:

[self.collectionView reloadDataWithCompletion:^{
    // reloadData is guaranteed to have completed
}];

Sehen Sie hier für eine Swift - Version dieser Antwort

Tyler Sheaffer
quelle
Dies ist der einzig richtige Weg. Es wurde meinem Projekt hinzugefügt, da ich die letzten Frames einiger Zellen für Animationszwecke benötigte. Ich habe auch für Swift hinzugefügt und bearbeitet. Hoffe es macht dir nichts aus 😉
Jon Vogel
2
Nachdem Sie den Block aufgerufen haben, layoutSubviewssollte er so eingestellt werden, dass nilnachfolgende Aufrufe von layoutSubviews, nicht unbedingt aufgrund eines Aufrufs reloadData, dazu führen, dass der Block ausgeführt wird, da eine starke Referenz gehalten wird, was nicht das gewünschte Verhalten ist.
Mark Bourke
Warum kann ich dies nicht für UITableView verwenden? Es wird keine sichtbare Oberfläche angezeigt. Ich habe die Header-Datei auch importiert, aber immer noch dieselbe
Julfikar
2
Ein Nachtrag zu dieser Antwort ist, dass es möglich ist, den vorhandenen Rückruf zu blockieren, wenn es nur einen gibt, was bedeutet, dass mehrere Anrufer eine Race-Bedingung haben. Die Lösung besteht darin, reloadDataCompletionBlockein Array von Blöcken zu erstellen und diese bei der Ausführung zu durchlaufen und das Array danach zu leeren.
Tyler Sheaffer
1) Entspricht dies nicht Robs erster Antwort, dh layoutIfNeeded zu verwenden? 2) Warum hast du iOS 10 erwähnt, funktioniert es nicht unter iOS 9?!
Honig
30

Ich hatte die gleichen Probleme wie Tyler Sheaffer.

Ich habe seine Lösung implementiert in Swift und sie hat meine Probleme gelöst.

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
    self.reloadData()
  }
}

Swift 2:

class UITableViewWithReloadCompletion: UITableView {

  var reloadDataCompletionBlock: (() -> Void)?

  override func layoutSubviews() {
    super.layoutSubviews()

    self.reloadDataCompletionBlock?()
    self.reloadDataCompletionBlock = nil
  }

  func reloadDataWithCompletion(completion:() -> Void) {
      reloadDataCompletionBlock = completion
      self.reloadData()
  }
}

Anwendungsbeispiel:

tableView.reloadDataWithCompletion() {
 // reloadData is guaranteed to have completed
}
Shawn Aukstak
quelle
1
nett! kleine nit-pick, können Sie entfernen, if letindem Sie sagen, reloadDataCompletionBlock?()die iff nicht nil 💥
Tyler Sheaffer
Kein Glück mit diesem in meiner Situation auf ios9
Matjan
self.reloadDataCompletionBlock? { completion() }hätte sein sollenself.reloadDataCompletionBlock?()
emem
Wie gehe ich mit der Größenänderung der Tabellenansichtshöhe um? Ich habe zuvor tableView.beginUpdates () tableView.layoutIfNeeded () tableView.endUpdates ()
Parth Tamane
10

Und eine UICollectionViewVersion, basierend auf der Antwort von kolaworld:

https://stackoverflow.com/a/43162226/1452758

Muss getestet werden. Funktioniert bisher unter iOS 9.2, Xcode 9.2 Beta 2, mit dem Scrollen einer CollectionView zu einem Index als Abschluss.

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()
    }
}

Verwendung:

myCollectionView.reloadDataThenPerform {
    myCollectionView.scrollToItem(at: indexPath,
            at: .centeredVertically,
            animated: true)
}
Womble
quelle
6

Es scheint, dass die Leute diese Frage und die Antworten immer noch lesen. B / c davon bearbeite ich meine Antwort, um das Wort Synchron zu entfernen für dieses wirklich irrelevant ist.

When [tableView reloadData]gibt zurück, die internen Datenstrukturen hinter der tableView wurden aktualisiert. Wenn die Methode abgeschlossen ist, können Sie daher sicher nach unten scrollen. Ich habe dies in meiner eigenen App überprüft. Die allgemein akzeptierte Antwort von @ rob-mayoff ist zwar auch in der Terminologie verwirrend, bestätigt dies jedoch in seinem letzten Update.

Wenn dein tableView nicht nach unten scrollen, liegt möglicherweise ein Problem mit einem anderen Code vor, den Sie nicht veröffentlicht haben. Vielleicht ändern Sie Daten, nachdem der Bildlauf abgeschlossen ist, und laden dann nicht neu und / oder scrollen dann nach unten?

Fügen Sie wie folgt eine Protokollierung hinzu, um zu überprüfen, ob die Tabellendaten danach korrekt sind reloadData. Ich habe den folgenden Code in einer Beispiel-App und es funktioniert perfekt.

// 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];
XJones
quelle
Ich habe meine Fragen aktualisiert. Wissen Sie, warum meine NSLogs so ausgeben würden?
Alan
8
reloadDataist nicht synchron. Früher war es - siehe diese Antwort: stackoverflow.com/a/16071589/193896
bendytree
1
Es ist synchron. Es ist sehr einfach, dies mit einer Beispiel-App zu testen und zu sehen. Sie haben in dieser Frage auf die Antwort von @ rob verlinkt. Wenn Sie sein Update unten lesen, hat er dies ebenfalls überprüft. Vielleicht sprechen Sie über die Änderungen des visuellen Layouts. Es ist wahr, dass die tableView nicht sichtbar synchron aktualisiert wird, sondern die Daten. Aus diesem Grund sind die vom OP benötigten Werte unmittelbar nach der reloadDataRückgabe korrekt .
XJones
1
Sie sind möglicherweise verwirrt darüber, was voraussichtlich passieren wird reloadData. Verwenden Sie meinen Testfall in viewWillAppearAkzeptieren für die scrollToRowAtIndexPath:Zeile b / c, die bedeutungslos ist, wenn die tableViewnicht angezeigt wird. Sie werden sehen, dass reloadDatadie in der tableViewInstanz zwischengespeicherten Daten aktualisiert wurden und dass dies reloadDatasynchron ist. Wenn Sie auf andere tableViewDelegatenmethoden verweisen, die beim tableViewLayout des Layouts aufgerufen werden , werden diese nicht aufgerufen, wenn das tableViewnicht angezeigt wird. Wenn ich Ihr Szenario falsch verstehe, erklären Sie es bitte.
XJones
3
Was für lustige Zeiten. Es ist 2014 und es gibt Streit darüber, ob eine Methode synchron und asynchron ist oder nicht. Fühlt sich an wie Vermutungen. Alle Implementierungsdetails sind hinter diesem Methodennamen völlig undurchsichtig. Ist das Programmieren nicht großartig?
Fatuhoku
5

Ich benutze diesen Trick, ziemlich sicher, dass ich ihn bereits in einem Duplikat dieser Frage gepostet habe:

-(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;
}
malhal
quelle
@ Fattie Es ist unklar, ob Sie es als positiven oder negativen Kommentar meinen. Aber ich habe gesehen, dass Sie eine andere Antwort kommentiert haben: "Dies scheint die beste Lösung zu sein!" Daher denke ich, dass Sie diese Lösung relativ gesehen nicht als die beste betrachten.
Cœur
1
Verlassen Sie sich auf einen Nebeneffekt einer gefälschten Animation? Auf keinen Fall ist das eine gute Idee. Lernen Sie, Selektor oder GCD auszuführen und richtig zu machen. Übrigens gibt es jetzt eine tabellengeladene Methode, die Sie einfach verwenden können, wenn Sie nichts dagegen haben, ein privates Protokoll zu verwenden, was wahrscheinlich in Ordnung ist, da es das Framework ist, das Ihren Code aufruft, und nicht umgekehrt.
Malhal
3

Eigentlich hat dieser mein Problem gelöst:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {

NSSet *visibleSections = [NSSet setWithArray:[[tableView indexPathsForVisibleRows] valueForKey:@"section"]];
if (visibleSections) {
    // hide the activityIndicator/Loader
}}
Asha Antony
quelle
2

Versuchen Sie es so, es wird funktionieren

[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

Ich werde ausführen, wenn die Tabelle vollständig geladen ist

Eine andere Lösung ist, dass Sie UITableView unterordnen können

Shashi3456643
quelle
1

Am Ende habe ich eine Variation von Shawns Lösung verwendet:

Erstellen Sie eine benutzerdefinierte UITableView-Klasse mit einem Delegaten:

protocol CustomTableViewDelegate {
    func CustomTableViewDidLayoutSubviews()
}

class CustomTableView: UITableView {

    var customDelegate: CustomTableViewDelegate?

    override func layoutSubviews() {
        super.layoutSubviews()
        self.customDelegate?.CustomTableViewDidLayoutSubviews()
    }
}

Dann benutze ich in meinem Code

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!!
    }
}

Stellen Sie außerdem sicher, dass Sie Ihre Tabellenansicht im Interface Builder auf CustomTableView setzen:

Geben Sie hier die Bildbeschreibung ein

Sam
quelle
Dies funktioniert, aber das Problem ist, dass die Methode jedes Mal getroffen wird, wenn eine einzelne Zelle geladen wird, NICHT DAS RELOAD DER GANZEN TABELLENANSICHT. Daher bezieht sich diese Antwort eindeutig nicht auf die gestellte Frage.
Yash Bedi
Es wird zwar mehrmals aufgerufen, aber nicht in jeder Zelle. Sie können also den ersten Delegaten anhören und den Rest ignorieren, bis Sie reloadData erneut aufrufen.
Sam
1

In Swift 3.0 + können wir eine Erweiterung für erstellen UITableViewmit einem escaped Closurewie unten:

extension UITableView {
    func reloadData(completion: @escaping () -> ()) {
        UIView.animate(withDuration: 0, animations: { self.reloadData()})
        {_ in completion() }
    }
}

Und verwenden Sie es wie unten, wo immer Sie wollen:

Your_Table_View.reloadData {
   print("reload done")
 }

hoffe, das wird jemandem helfen. Prost!

Chanaka Caldera
quelle
Geniale Idee ... nur um Verwirrung zu vermeiden, habe ich den Funktionsnamen t reload geändert, anstatt reloadData (). Vielen Dank
Vijay Kumar AB
1

Einzelheiten

  • Xcode Version 10.2.1 (10E1001), Swift 5

Lösung

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)
    }
}

Verwendung

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) }

Vollständige Probe

Vergessen Sie nicht, den Lösungscode hier hinzuzufügen

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
    }
}

Ergebnisse

Geben Sie hier die Bildbeschreibung ein

Wassili Bodnarchuk
quelle
0

Nur um einen anderen Ansatz anzubieten, basierend auf der Idee, dass die Fertigstellung die 'letzte sichtbare' Zelle ist, an die gesendet werden soll 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
}

Ein mögliches Problem ist: Wenn reloadData()der Vorgang vor dem lastIndexPathToDisplayFestlegen abgeschlossen wurde, wird die 'letzte sichtbare' Zelle angezeigt, bevor sie festgelegt lastIndexPathToDisplaywurde, und der Abschluss wird nicht aufgerufen (und befindet sich im Status 'Warten'):

self.mainTable.reloadData()

// cellForRowAt could be finished here, before setting `lastIndexPathToDisplay`

self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last

Wenn wir umkehren, könnte die Fertigstellung durch vorheriges Scrollen ausgelöst werden reloadData().

self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last

// cellForRowAt could trigger the completion by scrolling here since we arm 'lastIndexPathToDisplay' before 'reloadData()'

self.mainTable.reloadData()
bauerMusic
quelle
0

Versuche dies:

tableView.backgroundColor = .black

tableView.reloadData()

DispatchQueue.main.async(execute: {

    tableView.backgroundColor = .green

})

Die tableView-Farbe wird erst nach Abschluss der reloadData()Funktion von schwarz nach grün geändert .

Nirbhay Singh
quelle
0

Sie können die Funktion performBatchUpdates von uitableview verwenden

So können Sie erreichen

self.tableView.performBatchUpdates({

      //Perform reload
        self.tableView.reloadData()
    }) { (completed) in

        //Reload Completed Use your code here
    }
Noman Haroon
quelle
0

Erstellen einer wiederverwendbaren Erweiterung von CATransaction:

public extension CATransaction {
    static func perform(method: () -> Void, completion: @escaping () -> Void) {
        begin()
        setCompletionBlock {
            completion()
        }
        method()
        commit()
    }
}

Erstellen Sie nun eine Erweiterung von UITableView, die die Erweiterungsmethode von CATransaction verwendet:

public extension UITableView {
    func reloadData(completion: @escaping (() -> Void)) {
       CATransaction.perform(method: {
           reloadData()
       }, completion: completion)
    }
}

Verwendung:

tableView.reloadData(completion: {
    //Do the stuff
})
Umair
quelle
-2

Sie können es verwenden, um nach dem erneuten Laden von Daten etwas zu tun:

[UIView animateWithDuration:0 animations:^{
    [self.contentTableView reloadData];
} completion:^(BOOL finished) {
    _isUnderwritingUpdate = NO;
}];
Vit
quelle
-20

Versuchen Sie, Verzögerungen einzustellen:

[_tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0.2];
[_activityIndicator performSelector:@selector(stopAnimating) withObject:nil afterDelay:0.2];
Jake
quelle
14
Das ist gefährlich. Was ist, wenn das Nachladen länger dauert als Ihre Verzögerung?
Rob