Как узнать, что UICollectionView полностью загружен?


98

Мне нужно выполнить некоторую операцию всякий раз, когда UICollectionView был загружен полностью, т.е. в это время должны быть вызваны все методы источника данных / макета UICollectionView. Откуда я это знаю ?? Есть ли какой-либо метод делегата, чтобы узнать загруженный статус UICollectionView?

Ответы:


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
это вылетает для меня, когда я возвращаюсь назад, в остальном работает хорошо.
— ericosg

4
@ericosg добавить [self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL];к -viewWillDisappear:animated:а.
— pxpgraphics

Гениально! Большое спасибо!
— Abdo

41
Размер содержимого также изменяется при прокрутке, поэтому он ненадежен.
— Райан Хейтнер

Я пробовал использовать представление коллекции, которое получает данные с помощью метода async rest. Это означает, что при срабатывании этого метода Visible Cells всегда пуста, то есть при запуске я прокручиваю до элемента в коллекции.
— Чаки

155

Это сработало для меня:

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

Синтаксис Swift 4:

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

1
Работал у меня на iOS 9
— Magoo

5
@ G.Abhisek Errrr, конечно .... что тебе нужно объяснить? Первая строка reloadDataобновляет объект, collectionViewпоэтому он снова использует методы источника данных ... к performBatchUpdatesнему прикреплен блок завершения, поэтому, если оба выполняются в основном потоке, вы знаете, что любой код, который был вставлен вместо него, /// collection-view finished reloadбудет выполняться с новым и выложеннымcollectionView
— Magoo

8
Не работает у меня, когда у collectionView были ячейки с динамическим размером
— Ingaham

2
Этот метод - прекрасное решение без головной боли.
— arjavlad 07

1
@ingaham Это отлично работает для меня с ячейками динамического размера, Swift 4.2 IOS 12
— Romulo BM

27

На самом деле все очень просто.

Когда вы, например, вызываете метод reloadData UICollectionView или метод invalidateLayout его макета, вы делаете следующее:

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

Почему это работает:

В основном потоке (в котором мы должны выполнять все обновления пользовательского интерфейса) находится основная очередь, которая является последовательной по своей природе, то есть работает в режиме FIFO. Итак, в приведенном выше примере вызывается первый блок, в котором естьreloadData вызывается метод, за которым следует все остальное во втором блоке.

Теперь основной поток также блокируется. Так что, если для выполнения вам reloadDataтребуется 3 секунды, обработка второго блока будет отложена на эти 3 секунды.


2
На самом деле я только что протестировал это, блок вызывается раньше всех моих других методов делегата. Значит, не работает?
— taylorcressy 05

@taylorcressy, привет, я обновил код, который сейчас использую в двух производственных приложениях. Также включены объяснения.
— dezinezync

У меня не работает. Когда я вызываю reloadData, collectionView запрашивает ячейки после выполнения второго блока. Так что у меня та же проблема, что и у @taylorcressy.
— Рафаэль

1
Можете ли вы попробовать поместить второй вызов блока в первый? Непроверено, но может работать.
— dezinezync

6
reloadDataтеперь ставит в очередь загрузку ячеек в основном потоке (даже если вызывается из основного потока). Таким образом, ячейки фактически не загружаются ( cellForRowAtIndexPath:не вызываются) после reloadDataвозврата. Код-оболочка, который вы хотите выполнить после загрузки ваших ячеек, dispatch_async(dispatch_get_main...и вызов этого кода после вашего вызова reloadDataприведет к достижению желаемого результата.
— Tylerc230 02

12

Просто чтобы добавить к отличному ответу @dezinezync:

Swift 3+

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

@nikas это мы должны добавить ввиду появилось !!
— САРАТ САСИ,

1
Не обязательно, вы можете вызывать его из любого места, где хотите что-то сделать, когда макет наконец загружен.
— nikans 07

Я получал мерцание прокрутки, пытаясь заставить мою collectionView прокручивать в конце после вставки элементов, это исправило это, спасибо.
— Skoua

6

Делай это так:

       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

Попробуйте принудительно выполнить синхронный проход макета через layoutIfNeeded () сразу после вызова reloadData (). Кажется, работает как для UICollectionView, так и для UITableView на iOS 12.

collectionView.reloadData()
collectionView.layoutIfNeeded() 

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

Спасибо чувак! Я довольно долго искал решение этой проблемы.
— Trzy Gracje

3

Как ответил dezinezync , вам нужно отправить в основную очередь блок кода после reloadDataиз UITableViewилиUICollectionView , а затем этот блок будет выполнен после того, как клетки извлечение из

Чтобы сделать это более прямым при использовании, я бы использовал такое расширение:

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

Это также может быть реализовано UITableViewв


3

Другой подход с использованием RxSwift / RxCocoa:

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

1
Мне это нравится. performBatchUpdatesне работал в моем случае, это работает. Ура!
— Золтан

@ MichałZiobro, можно где нибудь обновить код? Метод должен вызываться только при изменении contentSize? Так что, если вы не меняете его на каждом свитке, это должно сработать!
— Чин Нгуен

1

Это работает для меня:

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

2
ерунда ... это никак не закончил выкладку.
— Magoo

1

Мне нужно было выполнить какое-то действие со всеми видимыми ячейками, когда представление коллекции загружается до того, как оно станет видимым для пользователя, я использовал:

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

Обратите внимание, что это будет вызываться при прокрутке представления коллекции, поэтому, чтобы избежать этих накладных расходов, я добавил:

private var souldPerformAction: Bool = true

и в самом действии:

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

Действие по-прежнему будет выполняться несколько раз, как количество видимых ячеек в исходном состоянии. но во всех этих вызовах у вас будет одинаковое количество видимых ячеек (все они). И логический флаг предотвратит его повторный запуск после того, как пользователь начал взаимодействовать с представлением коллекции.


1

Я просто сделал следующее, чтобы выполнить что-либо после перезагрузки представления коллекции. Вы можете использовать этот код даже в ответе API.

self.collectionView.reloadData()

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

1

Лучшее решение, которое я нашел до сих пор, - использовать CATransactionдля обработки завершения.

Swift 5 :

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

collectionView.reloadData()

CATransaction.commit()

0

Def сделать это:

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

}

Затем звоните так внутри своего ВК

let collection = MyCollectionView()

self.collection.reloadData(completion: {

})

Убедитесь, что вы используете подкласс !!


0

Эта работа для меня:


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

Просто перезагрузите collectionView внутри пакетных обновлений, а затем проверьте в блоке завершения, завершен он или нет, с помощью логического «finish».

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

0

Ниже приведен единственный подход, который у меня сработал.

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

-1

Вот как я решил проблему со Swift 3.0:

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

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

Это не сработает. VisibleCells будет иметь контент, как только будет загружена первая ячейка ... проблема в том, что мы хотим знать, когда была загружена последняя ячейка.
— Трэвис М.

-9

Попробуй это:

- (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
Это неверно, cellFor будет вызываться только в том случае, если эта ячейка будет видна, в этом случае последний видимый элемент не будет равен общему количеству элементов ...
— Джирун

-10

Вы можете сделать вот так ...

  - (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.

    }

Хотя этот код может ответить на вопрос, предоставление дополнительного контекста относительно того, почему и / или как он отвечает на вопрос, значительно улучшит его долгосрочную ценность. Пожалуйста, отредактируйте свой ответ, чтобы добавить пояснение.
— Тоби Спейт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.