Skąd mam wiedzieć, że UICollectionView został całkowicie załadowany?


98

Muszę wykonać jakąś operację za każdym razem, gdy UICollectionView został całkowicie załadowany, tj. W tym czasie należy wywołać wszystkie metody źródła danych / układu UICollectionView. Skąd mam to wiedzieć? Czy istnieje metoda delegata, aby poznać stan załadowania UICollectionView?

Odpowiedzi:


62
// In viewDidLoad
[self.collectionView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionOld context:NULL];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary  *)change context:(void *)context
{
    // You will get here when the reloadData finished 
}

- (void)dealloc
{
    [self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL];
}

1
to się zawiesza, kiedy wracam, w przeciwnym razie działa dobrze.
ericosg

4
@ericosg również dodaj [self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL];do -viewWillDisappear:animated:.
pxpgraphics

Znakomity! Dziękuję Ci bardzo!
Abdo

41
Rozmiar treści zmienia się również podczas przewijania, więc nie jest wiarygodny.
Ryan Heitner

Próbowałem uzyskać widok kolekcji, który pobiera dane przy użyciu metody odpoczynku asynchronicznego. Oznacza to, że widoczne komórki są zawsze puste, gdy ta metoda jest uruchamiana, co oznacza, że ​​na początku przewijam do elementu w kolekcji.
Chucky,

155

To zadziałało dla mnie:

[self.collectionView reloadData];
[self.collectionView performBatchUpdates:^{}
                              completion:^(BOOL finished) {
                                  /// collection-view finished reload
                              }];

Składnia Swift 4:

self.collectionView.reloadData()
self.collectionView.performBatchUpdates(nil, completion: {
    (result) in
    // ready
})

1
Pracował dla mnie na iOS 9
Magoo

5
@ G.Abhisek Errrr, jasne… co potrzebujesz wyjaśnienia? Pierwsza linia reloadDataodświeża, collectionViewwięc ponownie rysuje na swoich metodach źródła danych ... performBatchUpdatesma dołączony blok uzupełniania, więc jeśli oba są wykonywane w głównym wątku, znasz dowolny kod, który wstawiony w miejsce /// collection-view finished reloadwill, zostanie wykonany z nowym i ułożonymcollectionView
Magoo

8
Nie działa dla mnie, gdy collectionView miała komórki o dynamicznych rozmiarach
ingaham

2
Ta metoda jest idealnym rozwiązaniem bez bólu głowy.
arjavlad

1
@ingaham Działa to idealnie w przypadku komórek o dynamicznych rozmiarach, Swift 4.2 IOS 12
Romulo BM

27

W rzeczywistości jest to raczej bardzo proste.

Gdy na przykład wywołujesz metodę reloadData UICollectionView lub metodę invalidateLayout układu, wykonaj następujące czynności:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
});

dispatch_async(dispatch_get_main_queue(), ^{
    //your stuff happens here
    //after the reloadData/invalidateLayout finishes executing
});

Dlaczego to działa:

Główny wątek (w którym powinniśmy robić wszystkie aktualizacje interfejsu użytkownika) zawiera kolejkę główną, która ma charakter seryjny, czyli działa w stylu FIFO. W powyższym przykładzie zostaje wywołany pierwszy blok, który wywołuje naszą reloadDatametodę, a następnie cokolwiek innego w drugim bloku.

Teraz główny wątek również się blokuje. Więc jeśli reloadDatawykonanie zajmuje 3 sekundy, przetwarzanie drugiego bloku zostanie odroczone o te 3 sekundy.


2
Właśnie to przetestowałem. Blok jest wywoływany przed wszystkimi innymi metodami delegata. Więc to nie działa?
taylorcressy

@taylorcressy hej, zaktualizowałem kod, coś, czego używam teraz w 2 aplikacjach produkcyjnych. Zawiera również wyjaśnienie.
dezinezync

Nie działa na mnie. Kiedy wywołuję reloadData, collectionView żąda komórek po wykonaniu drugiego bloku. Więc mam ten sam problem, który wydaje się mieć @taylorcressy.
Raphael

1
Czy możesz spróbować umieścić drugą blokadę w pierwszej? Niezweryfikowane, ale może działać.
dezinezync

6
reloadDatateraz kolejkuje ładowanie komórek w głównym wątku (nawet jeśli są wywoływane z głównego wątku). Więc komórki nie są faktycznie ładowane ( cellForRowAtIndexPath:nie są wywoływane) po reloadDatapowrocie. Zawijanie kodu, który chcesz wykonać po załadowaniu komórek, dispatch_async(dispatch_get_main...i wywołanie go po wywołaniu reloadDataprzyniesie pożądany rezultat.
Tylerc230

12

Aby dodać do świetnej odpowiedzi @dezinezync:

Swift 3+

collectionView.collectionViewLayout.invalidateLayout() // or reloadData()
DispatchQueue.main.async {
    // your stuff here executing after collectionView has been layouted
}

@nikas to, które musimy dodać, pojawiło się !!
SARATH SASI

1
Niekoniecznie, możesz to wywołać z dowolnego miejsca, w którym chcesz coś zrobić, gdy układ zostanie ostatecznie załadowany.
nikans

Otrzymywałem migotanie przewijania, próbując przewinąć mój widok kolekcji na końcu po wstawieniu elementów, to rozwiązało problem, dzięki.
Skoua

6

Zrób to tak:

       UIView.animateWithDuration(0.0, animations: { [weak self] in
                guard let strongSelf = self else { return }

                strongSelf.collectionView.reloadData()

            }, completion: { [weak self] (finished) in
                guard let strongSelf = self else { return }

                // Do whatever is needed, reload is finished here
                // e.g. scrollToItemAtIndexPath
                let newIndexPath = NSIndexPath(forItem: 1, inSection: 0)
                strongSelf.collectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
        })

4

Spróbuj wymusić synchroniczne przekazywanie układu za pomocą layoutIfNeeded () zaraz po wywołaniu reloadData (). Wydaje się, że działa zarówno dla UICollectionView, jak i UITableView w systemie iOS 12.

collectionView.reloadData()
collectionView.layoutIfNeeded() 

// cellForItem/sizeForItem calls should be complete
completion?()

Dzięki! Dość długo szukałem rozwiązania tego problemu.
Trzy Gracje

3

Jak odpowiedział dezinezync , wystarczy wysłać do kolejki głównej blok kodu po reloadDataz UITableViewlub UICollectionViewi wtedy ten blok zostanie wykonany po dekolejkowaniu komórek

Aby było to prostsze podczas używania, użyłbym takiego rozszerzenia:

extension UICollectionView {
    func reloadData(_ completion: @escaping () -> Void) {
        reloadData()
        DispatchQueue.main.async { completion() }
    }
}

Może być również realizowane Do UITableViewtakże


3

Inne podejście przy użyciu RxSwift / RxCocoa:

        collectionView.rx.observe(CGSize.self, "contentSize")
            .subscribe(onNext: { size in
                print(size as Any)
            })
            .disposed(by: disposeBag)

1
Podoba mi się to. performBatchUpdatesnie działał w moim przypadku, ten to robi. Twoje zdrowie!
Zoltán

@ MichałZiobro, Czy możesz gdzieś zaktualizować swój kod? Metodę należy wywołać tylko wtedy, gdy zmieniono contentSize? Więc jeśli nie zmienisz tego na każdym zwoju, to powinno działać!
Chinh Nguyen

1

To działa dla mnie:

__weak typeof(self) wself= self;
[self.contentCollectionView performBatchUpdates:^{
    [wself.contentCollectionView reloadData];
} completion:^(BOOL finished) {
    [wself pageViewCurrentIndexDidChanged:self.contentCollectionView];
}];

2
nonsens ... to w żaden sposób się nie skończyło.
Magoo

1

Musiałem wykonać jakąś akcję na wszystkich widocznych komórkach, gdy widok kolekcji zostanie załadowany, zanim będzie widoczny dla użytkownika, użyłem:

public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    if shouldPerformBatch {
        self.collectionView.performBatchUpdates(nil) { completed in
            self.modifyVisibleCells()
        }
    }
}

Zwróć uwagę, że zostanie to wywołane podczas przewijania widoku kolekcji, więc aby temu zapobiec, dodałem:

private var souldPerformAction: Bool = true

aw samej akcji:

private func modifyVisibleCells() {
    if self.shouldPerformAction {
        // perform action
        ...
        ...
    }
    self.shouldPerformAction = false
}

Akcja będzie nadal wykonywana wiele razy, ponieważ liczba widocznych komórek w stanie początkowym. ale we wszystkich tych wywołaniach będziesz mieć taką samą liczbę widocznych komórek (wszystkie). A flaga logiczna uniemożliwi ponowne uruchomienie po rozpoczęciu interakcji użytkownika z widokiem kolekcji.


1

Wykonałem następujące czynności, aby wykonać cokolwiek po ponownym załadowaniu widoku kolekcji. Możesz użyć tego kodu nawet w odpowiedzi API.

self.collectionView.reloadData()

DispatchQueue.main.async {
   // Do Task after collection view is reloaded                
}

1

Najlepszym rozwiązaniem, jakie do tej pory znalazłem, jest CATransactionobsługa kompletacji.

Swift 5 :

CATransaction.begin()
CATransaction.setCompletionBlock {
    // UICollectionView is ready
}

collectionView.reloadData()

CATransaction.commit()

0

Def zrób to:

//Subclass UICollectionView
class MyCollectionView: UICollectionView {

    //Store a completion block as a property
    var completion: (() -> Void)?

    //Make a custom funciton to reload data with a completion handle
    func reloadData(completion: @escaping() -> Void) {
        //Set the completion handle to the stored property
        self.completion = completion
        //Call super
        super.reloadData()
    }

    //Override layoutSubviews
    override func layoutSubviews() {
        //Call super
        super.layoutSubviews()
        //Call the completion
        self.completion?()
        //Set the completion to nil so it is reset and doesn't keep gettign called
        self.completion = nil
    }

}

Następnie zadzwoń w ten sposób w swoim VC

let collection = MyCollectionView()

self.collection.reloadData(completion: {

})

Upewnij się, że używasz podklasy !!


0

Ta praca dla mnie:


- (void)viewDidLoad {
    [super viewDidLoad];

    int ScrollToIndex = 4;

    [self.UICollectionView performBatchUpdates:^{}
                                    completion:^(BOOL finished) {
                                             NSIndexPath *indexPath = [NSIndexPath indexPathForItem:ScrollToIndex inSection:0];
                                             [self.UICollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
                                  }];

}


0

Po prostu przeładuj kolekcję Wyświetl aktualizacje wsadowe, a następnie sprawdź w bloku uzupełniania, czy zostało to zakończone, czy nie, za pomocą boolowskiego „finish”.

self.collectionView.performBatchUpdates({
        self.collectionView.reloadData()
    }) { (finish) in
        if finish{
            // Do your stuff here!
        }
    }

0

Poniżej znajduje się jedyne podejście, które zadziałało dla mnie.

extension UICollectionView {
    func reloadData(_ completion: (() -> Void)? = nil) {
        reloadData()
        guard let completion = completion else { return }
        layoutIfNeeded()
        completion()
    }
}

-1

Oto jak rozwiązałem problem ze Swift 3.0:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    if !self.collectionView.visibleCells.isEmpty {
        // stuff
    }
}

To nie zadziała. VisibleCells będą miały zawartość zaraz po załadowaniu pierwszej komórki ... Problem polega na tym, że chcemy wiedzieć, kiedy załadowano ostatnią komórkę.
Travis M.

-9

Spróbuj tego:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return _Items.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell;
    //Some cell stuff here...

    if(indexPath.row == _Items.count-1){
       //THIS IS THE LAST CELL, SO TABLE IS LOADED! DO STUFF!
    }

    return cell;
}

2
To nie jest poprawne, funkcja cellFor zostanie wywołana tylko wtedy, gdy ta komórka będzie widoczna, w takim przypadku ostatni widoczny element nie będzie równy całkowitej liczbie elementów ...
Jirune

-10

Możesz to zrobić ...

  - (void)reloadMyCollectionView{

       [myCollectionView reload];
       [self performSelector:@selector(myStuff) withObject:nil afterDelay:0.0];

   }

  - (void)myStuff{
     // Do your stuff here. This will method will get called once your collection view get loaded.

    }

Chociaż ten kod może odpowiedzieć na pytanie, dostarczenie dodatkowego kontekstu dotyczącego tego, dlaczego i / lub w jaki sposób odpowiada na pytanie, znacznie poprawiłoby jego długoterminową wartość. Proszę edytować swoje odpowiedzi, aby dodać trochę wyjaśnień.
Toby Speight
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.