Как узнать, что 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.