【问题标题】:Swift: How to refresh UICollectionView layout after rotation of the deviceSwift:如何在设备旋转后刷新 UICollectionView 布局
【发布时间】:2016-08-21 10:18:45
【问题描述】:

我使用 UICollectionView (flowlayout) 构建了一个简单的布局。 使用self.view.frame.width将每个单元格的宽度设置为屏幕宽度

但是当我旋转设备时,单元格没有得到更新。

我找到了一个函数,它会在方向改变时调用:

override func willRotateToInterfaceOrientation(toInterfaceOrientation: 
  UIInterfaceOrientation, duration: NSTimeInterval) {
    //code
}

但我找不到更新 UICollectionView 布局的方法

主要代码在这里:

class ViewController: UIViewController , UICollectionViewDelegate , UICollectionViewDataSource , UICollectionViewDelegateFlowLayout{

    @IBOutlet weak var myCollection: UICollectionView!

    var numOfItemsInSecOne: Int!
    override func viewDidLoad() {
        super.viewDidLoad()

        numOfItemsInSecOne = 8
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {

        //print("orientation Changed")
    }

    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return numOfItemsInSecOne
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cellO", forIndexPath: indexPath)

        return cell
    }

    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize{
    let itemSize = CGSize(width: self.view.frame.width, height: 100)
    return itemSize
    }}

【问题讨论】:

    标签: ios swift uicollectionview flowlayout


    【解决方案1】:

    添加此功能:

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews() 
        myCollection.collectionViewLayout.invalidateLayout()
    }
    

    当你改变方向时,这个函数会被调用。

    【讨论】:

    • UICollectionViewController 已经有collectionView 属性,所以你只需按照我所说的调用它们。只需将它们编辑为collectionView.reloadData()。它会起作用的。
    • 所以需要重新加载视图,不能只触发一次布局刷新?
    • 检查@kelin 的回答.invalidateLayout() 很有意义。
    • 应用在应用启动后挂起。
    • 此解决方案效率不高,因为当用户滚动UICollectionView 的单元格时会多次调用viewDidLayoutSubviews()
    【解决方案2】:

    更好的选择是调用invalidateLayout() 而不是reloadData(),因为它不会强制重新创建单元格,因此性能会稍微好一些:

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews() 
        myCollection.collectionViewLayout.invalidateLayout()
    }
    

    【讨论】:

    • 这是正确的方法。这应该是公认的答案。
    • 这是正确的做法......不是再次重新加载数据
    • 这引起了我的迷恋。当我在 viewDidLayoutSubviews 函数中放置一个打印语句时识别出一个无限循环。
    • @nyxee,然后尝试viewWillLayoutSubviews。我敢打赌,您的 Collection View 是 View Controller 的视图?如果是这样,我建议将其包装到另一个视图中。
    • 如果使用UICollectionViewController,则会出现无限循环,因为在这种情况下collectionView 也是控制器的视图。所以如果你改变collectionView布局viewWillLayoutSubviews,相关方法就会被调用。
    【解决方案3】:

    您也可以通过这种方式使其无效。

    - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
        [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    
        [self.collectionView.collectionViewLayout invalidateLayout]; 
    }
    

    【讨论】:

      【解决方案4】:

      viewWillLayoutSubviews() 对我不起作用。 viewDidLayoutSubviews() 也没有。两者都使应用程序进入无限循环,我使用打印命令进行了检查。

      其中一种工作方式是

      override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
      // Reload here
      }
      

      【讨论】:

      • 别忘了致电super.viewWillTransition...
      【解决方案5】:

      要更新UICollectionViewLayout,也可以使用traitCollectionDidChange方法:

      override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
          super.traitCollectionDidChange(previousTraitCollection)
      
          guard previousTraitCollection != nil else { return }
          collectionView?.collectionViewLayout.invalidateLayout()
      }
      

      【讨论】:

      • 这对我来说效果最好,所需的方法实际上取决于您的 CollectionView 应用程序,在我的实例中,这与我的自定义 FlowLayout 的其他行为比其他解决方案更兼容,因为我的项目都有相同的大小取决于 traitCollection
      • 很棒 - 在自定义 UITableViewCell 中实例化 UICollectionView 时也可以使用。
      • 这个解决方案在 iPad 上不起作用,因为水平 trait 和垂直 trait 没有改变。
      【解决方案6】:

      UICollectionLayout 检测到边界更改时,它会询问是否需要重新路由 Invalidate 布局。可以直接重写方法。UICollectionLayout可以适时调用invalidateLayout方法

      class CollectionViewFlowLayout: UICollectionViewFlowLayout{
          
          /// The default implementation of this method returns false.
          /// Subclasses can override it and return an appropriate value
          /// based on whether changes in the bounds of the collection
          /// view require changes to the layout of cells and supplementary views.
          /// If the bounds of the collection view change and this method returns true,
          /// the collection view invalidates the layout by calling the invalidateLayout(with:) method.
          override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
              
              return (self.collectionView?.bounds ?? newBounds) != newBounds
          }
      }
      

      【讨论】:

        【解决方案7】:

        请理解

        traitCollectionDidChange(previousTraitCollection :) 旋转时不会在 iPad 上调用,因为尺寸类在纵向和横向都是 .regular 。每当集合视图大小发生变化时都会调用 viewWillTransition(to:with:)。

        此外,如果您的应用支持多任​​务处理,则不应使用 UIScreen.mainScreen().bounds,因为它可能不会占据整个屏幕,最好使用 collectionView.frame.width。

        【讨论】:

        • TanX!!!你救了我几个小时!我将UIScreen.main.bounds.width 更改为collectionView.frame.width,然后砰!固定!
        • 欢迎您@HamedGhadirian 享受??
        【解决方案8】:

        我的代码:

        override func viewWillLayoutSubviews() {
           super.viewWillLayoutSubviews()
           self.collectionView.collectionViewLayout.invalidateLayout()
        }
        

        会正常工作!!!

        【讨论】:

        【解决方案9】:

        调用viewWillLayoutSubviews 不是最佳选择。尝试先调用invalidateLayout() 方法。

        如果您遇到The behaviour of the UICollectionViewFlowLayout is not defined 错误,您需要根据新布局验证视图中的所有元素是否已更改其大小。 (参见示例代码中的可选步骤)

        这里是代码,可以帮助您入门。根据创建 UI 的方式,您可能需要尝试找到正确的视图来调用 recalculate 方法,但这应该会引导您迈出第一步。

        override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        
            super.viewWillTransition(to: size, with: coordinator)
        
            /// (Optional) Additional step 1. Depending on your layout, you may have to manually indicate that the content size of a visible cells has changed
            /// Use that step if you experience the `the behavior of the UICollectionViewFlowLayout is not defined` errors.
        
            collectionView.visibleCells.forEach { cell in
                guard let cell = cell as? CustomCell else {
                    print("`viewWillTransition` failed. Wrong cell type")
                    return
                }
        
                cell.recalculateFrame(newSize: size)
        
            }
        
            /// (Optional) Additional step 2. Recalculate layout if you've explicitly set the estimatedCellSize and you'll notice that layout changes aren't automatically visible after the #3
        
            (collectionView.collectionViewLayout as? CustomLayout)?.recalculateLayout(size: size)
        
        
            /// Step 3 (or 1 if none of the above is applicable)
        
            coordinator.animate(alongsideTransition: { context in
                self.collectionView.collectionViewLayout.invalidateLayout()
            }) { _ in
                // code to execute when the transition's finished.
            }
        
        }
        
        /// Example implementations of the `recalculateFrame` and `recalculateLayout` methods:
        
            /// Within the `CustomCell` class:
            func recalculateFrame(newSize: CGSize) {
                self.frame = CGRect(x: self.bounds.origin.x,
                                    y: self.bounds.origin.y,
                                    width: newSize.width - 14.0,
                                    height: self.frame.size.height)
            }
        
            /// Within the `CustomLayout` class:
            func recalculateLayout(size: CGSize? = nil) {
                estimatedItemSize = CGSize(width: size.width - 14.0, height: 100)
            }
        
            /// IMPORTANT: Within the `CustomLayout` class.
            override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        
                guard let collectionView = collectionView else {
                    return super.shouldInvalidateLayout(forBoundsChange: newBounds)
                }
        
                if collectionView.bounds.width != newBounds.width || collectionView.bounds.height != newBounds.height {
                    return true
                } else {
                    return false
                }
            }
        

        【讨论】:

          【解决方案10】:

          您可以使用更新您的 UICollectionView 布局

          func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
              if isLandscape {
                  return CGSizeMake(yourLandscapeWidth, yourLandscapeHeight)
              }
              else {
                  return CGSizeMake(yourNonLandscapeWidth, yourNonLandscapeHeight)
              }
          }
          

          【讨论】:

            【解决方案11】:

            它对我有用。这是 Objective-C 中的代码:

            - (void)viewDidLayoutSubviews {
              [super viewDidLayoutSubviews];
              [collectionView.collectionViewLayout invalidateLayout];
            }
            

            【讨论】:

              【解决方案12】:

              我也遇到了一些问题,但后来解决了:

              override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
                      collectionViewFlowLayoutSetup(with: view.bounds.size.width)
                      collectionView?.collectionViewLayout.invalidateLayout()
                      collectionViewFlowLayoutSetup(with: size.width)
                  }
              
                  fileprivate func collectionViewFlowLayoutSetup(with Width: CGFloat){
              
                      if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout {
                          flowLayout.estimatedItemSize = CGSize(width: Width, height: 300)
                      }
              
                  }
              

              【讨论】:

                【解决方案13】:

                我通过在屏幕方向更改时设置通知并重新加载根据屏幕方向设置项目大小的单元格并将索引路径设置为前一个单元格来解决此问题。这也适用于 flowlayout。这是我写的代码:

                var cellWidthInLandscape: CGFloat = 0 {
                    didSet {
                        self.collectionView.reloadData()
                    }
                }
                
                var lastIndex: Int = 0
                
                override func viewDidLoad() {
                    super.viewDidLoad()
                
                    collectionView.dataSource = self
                    collectionView.delegate = self
                    NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
                    cellWidthInLandscape = UIScreen.main.bounds.size.width
                
                }
                deinit {
                    NotificationCenter.default.removeObserver(self)
                }
                @objc func rotated() {
                
                        // Setting new width on screen orientation change
                        cellWidthInLandscape = UIScreen.main.bounds.size.width
                
                       // Setting collectionView to previous indexpath
                        collectionView.scrollToItem(at: IndexPath(item: lastIndex, section: 0), at: .right, animated: false)
                }
                    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
                        NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
                
                }
                
                func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
                
                   // Getting last contentOffset to calculate last index of collectionViewCell
                    lastIndex = Int(scrollView.contentOffset.x / collectionView.bounds.width)
                }
                
                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
                        // Setting new width of collectionView Cell
                        return CGSize(width: cellWidthInLandscape, height: collectionView.bounds.size.height)
                
                }
                

                【讨论】:

                  【解决方案14】:

                  我用下面的方法解决了这个问题

                  override func viewDidLayoutSubviews() {
                          if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
                              collectionView.collectionViewLayout.invalidateLayout()
                              collectionView.collectionViewLayout = flowLayout
                          }
                      }
                  

                  【讨论】:

                    【解决方案15】:

                    除了其他人已经提到的,如果你自己创建集合视图而不是继承 UICollectionViewController,不要忘记设置:

                    collectionView.translatesAutoresizingMaskIntoConstraints = false
                    

                    设置约束时。

                    【讨论】:

                      【解决方案16】:

                      试试这个:

                      class CollectionViewFlowLayout: UICollectionViewFlowLayout {
                      
                      override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
                          let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
                          if let collectionView = collectionView {
                              context.invalidateFlowLayoutDelegateMetrics = collectionView.bounds.size != newBounds.size
                          }
                          return context
                        }
                      
                      }
                      

                      文档:

                      此属性的默认值为 false。如果您因更改任何项目的大小而使布局无效,请将此属性设置为 true。当此属性设置为 true 时,流布局对象会重新计算其项目和视图的大小,并根据需要查询委托对象以获取该信息。

                      【讨论】:

                        猜你喜欢
                        • 2016-05-15
                        • 2017-05-29
                        • 2013-11-17
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2016-03-12
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多