【问题标题】:How do I know that the UICollectionView has been loaded completely?我怎么知道 UICollectionView 已经完全加载了?
【发布时间】:2012-12-10 19:09:06
【问题描述】:

每当 UICollectionView 完全加载时,我都必须进行一些操作,即那时应该调用所有 UICollectionView 的数据源/布局方法。我怎么知道??是否有任何委托方法可以知道 UICollectionView 加载状态?

【问题讨论】:

    标签: uicollectionview delegates reload


    【解决方案1】:

    由于UICollectionView 的异步特性令人困惑,这里的大多数解决方案都不可靠或具有不确定的行为(这可能会导致随机错误)。

    一个可靠的解决方案是继承UICollectionView 以在layoutSubviews() 的末尾运行一个完成块。

    Objectice-C 中的代码: https://stackoverflow.com/a/39648633

    Swift 中的代码: https://stackoverflow.com/a/39798079

    【讨论】:

      【解决方案2】:

      这对我有用:

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

      Swift 4 语法:

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

      【讨论】:

      • 在 iOS 9 上为我工作
      • @G.Abhisek Errrr,当然……你需要解释什么?第一行reloadData 刷新了collectionView,因此它再次利用了它的数据源方法...performBatchUpdates 附加了一个完成块,因此如果两者都在主线程上执行,您知道任何代替@ 的代码987654326@ 将执行一个新的布局 collectionView
      • 当 collectionView 有动态大小的单元格时对我不起作用
      • 这种方法是完美的解决方案,没有任何头痛。
      • @ingaham 这对我来说非常适合动态大小的单元格,Swift 4.2 IOS 12
      【解决方案3】:

      到目前为止我发现的最佳解决方案是使用CATransaction 来处理完成。

      Swift 5

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

      更新: 上述解决方案似乎在某些情况下有效,而在某些情况下则无效。我最终使用了公认的答案,这绝对是最稳定和经过验证的方法。这是 Swift 5 版本:

      private var contentSizeObservation: NSKeyValueObservation?
      
      contentSizeObservation = collectionView.observe(\.contentSize) { [weak self] _, _ in
            self?.contentSizeObservation = nil
            completion()
      }
      
      collectionView.reloadData()
      

      【讨论】:

        【解决方案4】:

        SWIFT 5

        override func viewDidLoad() {
            super.viewDidLoad()
            
            // "collectionViewDidLoad" for transitioning from product's cartView to it's cell in that view
            self.collectionView?.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.new, context: nil)
        }
        
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            if let observedObject = object as? UICollectionView, observedObject == self.collectionView {
                print("collectionViewDidLoad")
                self.collectionView?.removeObserver(self, forKeyPath: "contentSize")
            }
        }
        

        【讨论】:

          【解决方案5】:

          以下是唯一对我有用的方法。

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

          【讨论】:

          • 我觉得不行,因为 reloaData 会立即给予控制权
          【解决方案6】:

          我只是在重新加载集合视图后执行以下操作。您甚至可以在 API 响应中使用此代码。

          self.collectionView.reloadData()
          
          DispatchQueue.main.async {
             // Do Task after collection view is reloaded                
          }
          

          【讨论】:

            【解决方案7】:

            只需在批量更新中重新加载collectionView,然后在布尔“完成”的帮助下检查完成块是否完成。

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

            【讨论】:

            • 不适合我,至少在 iOS 14.1 模拟器中。永远不会调用完成。请参阅此雷达:openradar.me/48941363,这似乎是相关的。另外,请参阅SO,这表明从 performBatchUpdates 中调用 reloadData() 不是一个好主意。我对这种优雅的方法寄予厚望......
            【解决方案8】:

            尝试在 reloadData() 调用之后立即通过 layoutIfNeeded() 强制同步布局传递。似乎适用于 iOS 12 上的 UICollectionView 和 UITableView。

            collectionView.reloadData()
            collectionView.layoutIfNeeded() 
            
            // cellForItem/sizeForItem calls should be complete
            completion?()
            

            【讨论】:

            • 谢谢你!我一直在寻找这个问题的解决方案。
            【解决方案9】:

            这对我有用:

            
            - (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];
                                              }];
            
            }
            
            

            【讨论】:

              【解决方案10】:

              使用 RxSwift/RxCocoa 的不同方法:

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

              【讨论】:

              • 我喜欢这个。 performBatchUpdates 在我的情况下不起作用,这个可以。干杯!
              • @MichałZiobro,你能在某处更新你的代码吗?仅当 contentSize 更改时才应调用该方法?因此,除非您在每次滚动时都更改它,否则应该可以!
              • 这对我来说太棒了,它不仅滚动到需要的单元格,而且不断向后滚动(因为它可以在内容大小不断变化的情况下工作),我想这不是一个错误,而是一个特性 xD。但是,这并不是很明确的解决方案
              【解决方案11】:

              正如dezinezync 回答的那样,您需要从UITableViewUICollectionViewreloadData 之后的代码块分派到主队列,然后在单元格出列后执行此块

              为了在使用时更直接,我会使用这样的扩展:

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

              也可以实现为UITableView

              【讨论】:

                【解决方案12】:

                当集合视图在用户可见之前被加载时,我需要对所有可见单元格执行一些操作,我使用了:

                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
                }
                

                该动作仍会执行多次,作为初始状态下可见单元格的数量。但是在所有这些调用中,您将拥有相同数量的可见单元格(全部)。并且布尔标志将阻止它在用户开始与集合视图交互后再次运行。

                【讨论】:

                  【解决方案13】:

                  默认这样做:

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

                  然后在你的 VC 中这样调用

                  let collection = MyCollectionView()
                  
                  self.collection.reloadData(completion: {
                  
                  })
                  

                  确保您使用的是子类!!

                  【讨论】:

                    【解决方案14】:

                    只是为了添加一个很棒的@dezinezync 答案:

                    斯威夫特 3+

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

                    【讨论】:

                    • @nikas 我们必须在视图中添加的这个确实出现了!!
                    • 不一定,你可以在最终加载布局时从任何你想做的地方调用它。
                    • 我在插入项目后试图让我的 collectionView 滚动到最后,这解决了它,谢谢。
                    【解决方案15】:

                    这就是我解决 Swift 3.0 问题的方法:

                    override func viewDidAppear(_ animated: Bool) {
                        super.viewDidAppear(animated)
                    
                        if !self.collectionView.visibleCells.isEmpty {
                            // stuff
                        }
                    }
                    

                    【讨论】:

                    • 这行不通。 VisibleCells 将在第一个单元格加载后立即包含内容...问题是我们想知道最后一个单元格何时加载。
                    【解决方案16】:

                    这样做:

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

                    【讨论】:

                      【解决方案17】:

                      这对我有用:

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

                      【讨论】:

                      • 废话......这还没有完成任何方式的布局。
                      【解决方案18】:
                      // 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];
                      }
                      

                      【讨论】:

                      • 这会在我返回时崩溃,否则效果很好。
                      • @ericosg 也将[self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL]; 添加到-viewWillDisappear:animated:
                      • 太棒了!非常感谢!
                      • 滚动时内容大小也会发生变化,因此不可靠。
                      • 我尝试了一个集合视图,它使用异步休息方法获取它的数据。这意味着当此方法触发时,Visible Cells 始终为空,这意味着我在开始时滚动到集合中的一个项目。
                      【解决方案19】:

                      其实很简单。

                      例如,当您调用 UICollectionView 的 reloadData 方法或它的布局的 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
                      });
                      

                      为什么会这样:

                      主线程(我们应该在其中进行所有 UI 更新)包含主队列,它本质上是串行的,即它以 FIFO 方式工作。所以在上面的例子中,第一个块被调用,我们的 reloadData 方法被调用,然后是第二个块中的任何其他内容。

                      现在主线程也被阻塞了。因此,如果您是 reloadData 需要 3s 执行,则第二个块的处理将被这些 3s 延迟。

                      【讨论】:

                      • 我实际上只是测试了这个,该块在我所有其他委托方法之前被调用。所以它不起作用?
                      • @taylorcressy 嘿,我已经更新了代码,这是我现在在 2 个生产应用程序中使用的东西。还包括解释。
                      • 对我不起作用。当我调用 reloadData 时,collectionView 在执行第二个块后请求单元格。所以我遇到了和@taylorcressy 一样的问题。
                      • 您可以尝试将第二个块调用放在第一个块中吗?未经验证,但可能有效。
                      • reloadData 现在将单元格的加载排入主线程(即使从主线程调用)。所以在reloadData 返回后,单元格实际上并没有被加载(cellForRowAtIndexPath: 没有被调用)。在将单元格加载到 dispatch_async(dispatch_get_main... 后包装您要执行的代码,并在您调用 reloadData 之后调用该代码将获得所需的结果。
                      【解决方案20】:

                      试试这个:

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

                      【讨论】:

                      • 这是不正确的,只有当该单元格可见时才会调用 cellFor,在这种情况下,最后一个可见项目将不等于项目总数...
                      【解决方案21】:

                      你可以这样做......

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

                      【讨论】:

                      • 虽然此代码可能会回答问题,但提供有关 why 和/或 如何 回答问题的额外上下文将显着改善其长期价值。请edit你的答案添加一些解释。
                      猜你喜欢
                      • 1970-01-01
                      • 2016-09-28
                      • 1970-01-01
                      • 2017-04-17
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多