Swift:设备旋转后如何刷新UICollectionView布局

90

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

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

enter image description here

我发现了一个函数,在方向变化时被调用:

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
    }}
17个回答

103

添加此函数:

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

当您更改方向时,将调用此函数。

2
UICollectionViewController已经有了collectionView属性,所以你只需要按照我说的调用它们。将它们编辑成collectionView.reloadData()即可。这样就可以工作了。 - Twitter khuong291
2
所以你需要重新加载视图,不能只触发布局刷新吗? - Rivera
2
检查@kelin的答案。.invalidateLayout()非常有意义。 - geekay
5
使用后,应用程序在启动时出现卡顿现象。 - rv7284
4
当用户滚动UICollectionView的单元格时,viewDidLayoutSubviews()被调用多次,因此这个解决方案不够有效。 - Imanou Petit
显示剩余11条评论

71

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

更佳選擇是調用invalidateLayout() 而非 reloadData() ,因為它不會強制重建儲存格,因此性能會稍微更好:

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

这是正确的做法,不要重新加载数据。 - Abdul Waheed
9
引起了我的热望,当我在viewDidLayoutSubviews函数中添加一个打印语句时,发现了一个无限循环。 - nyxee
2
@nyxee,可以尝试使用viewWillLayoutSubviews。我猜你的集合视图是视图控制器的视图?如果是这样,我建议将其包装在另一个视图中。 - kelin
谢谢。我会在某个时候回来看这个问题。现在我已经转换到tableViews了。之前我一直在使用UICollectionViewController。 - nyxee
6
如果您使用UICollectionViewController,可能会发生无限循环,因为在这种情况下,collectionView也是控制器的视图。因此,如果您更改collectionView的布局,将调用viewWillLayoutSubviews和相关方法。 - kelin
显示剩余5条评论

20

你也可以通过这种方式使其失效。

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

    [self.collectionView.collectionViewLayout invalidateLayout]; 
}

19

viewWillLayoutSubviews()和viewDidLayoutSubviews()都没有起作用,两者都导致应用程序进入无限循环,我使用print命令进行了检查。

其中一种可行的方法是

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

21
记得调用 super.viewWillTransition... - d4Rk

17

UICollectionLayout 检测到边界更改时,它会询问是否需要重新路由无效布局。您可以直接重写该方法。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
    }
}

14

可以使用traitCollectionDidChange方法来更新UICollectionViewLayout

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    guard previousTraitCollection != nil else { return }
    collectionView?.collectionViewLayout.invalidateLayout()
}

2
这对我来说效果最好,所需的方法取决于您对CollectionView的应用,对于我的情况,这与其他自定义FlowLayout的行为更兼容,因为我的所有项目都具有依赖于traitCollection的相同大小。 - Rocket Garden
3
很棒 - 它也适用于在自定义的UITableViewCell中实例化UICollectionView。 - DrWhat
2
此解决方案在iPad上无法使用,因为水平特性和垂直特性都没有改变。 - tphduy

12

请了解这个事情

当iPad旋转时,traitCollectionDidChange(previousTraitCollection :)不会被调用,因为纵向和横向方向的size class都是.regular。但是,每当collection view大小更改时,将调用viewWillTransition(to:with:)。

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


1
谢谢!你帮我省了好几个小时!我把UIScreen.main.bounds.width改为collectionView.frame.width,问题解决了! - Hamed Ghadirian
你好 @HamedGhadirian,欢迎。 - Mehul

8

我的代码:

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

可以正常工作!!!


1
这是与@kelin的答案相同的答案 https://dev59.com/CFoU5IYBdhLWcg3w9aei#42174492 - Lance Samaria

5
调用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
        }
    }

3

您可以使用以下方法更新您的UICollectionView布局:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接