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

我使用 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
}}
105108 次浏览

Add this function:

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

When you change the orientation, this function would be called.

you can update your UICollectionView Layout by using

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

The better option is to call invalidateLayout() instead of reloadData() because it will not force recreation of the cells, so performance will be slightly better:

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

Also you can invalidate it in this way.

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


[self.collectionView.collectionViewLayout invalidateLayout];
}

To update UICollectionViewLayout, traitCollectionDidChange method might be used as well:

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


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

The viewWillLayoutSubviews() did not work for me. Neither did viewDidLayoutSubviews(). Both made the app go into an infinite loop which I checked using a print command.

One of the ways that do work is

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

I was also having some problem but then it got solved using :

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


}

I solve this by setting notification when screen orientation changes and reloading cell which set itemsize according to screen orientation and setting indexpath to previous cell. This does work with flowlayout too. Here is the code i wrote:

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)


}

Calling viewWillLayoutSubviews is not optimal. Try calling the invalidateLayout() method first.

If you experience the The behaviour of the UICollectionViewFlowLayout is not defined error, you need to verify if all the elements within your view's have changed its sizes, according to a new layout. (see the optional steps within the example code)

Here is the code, to get you started. Depending on the way your UI is created, you may have to experiment to find the right view to call the recalculate method, yet that should guide you towards your first steps.

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

I solved the issue using below method

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

When UICollectionLayout detects a bounds change, it asks if it needs to reroute the Invalidate layout. You can rewrite the method directly.UICollectionLayout can call invalidateLayout method at the right time

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

It works for me. And this is code in Objective-C:

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

Please understand this

traitCollectionDidChange(previousTraitCollection :) won't be called on iPad when it is rotated, because size class is .regular in both portrait and landscape orientations. There's viewWillTransition(to:with:) that will be called whenever collection view size changes.

Also you shouldn't use UIScreen.mainScreen().bounds if your app supports multitasking as it might not occupy the whole screen, better use collectionView.frame.width for that.

My Code :

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

Will work correctly!!!

Try this:

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
}


}

Documentation :

The default value of this property is false. Set this property to true if you are invalidating the layout because of changes to the size of any items. When this property is set to true, the flow layout object recomputes the size of its items and views, querying the delegate object as needed for that information.

In addition to what everybody else already mentioned, if you are creating the collection view yourself instead of subclassing UICollectionViewController, don't forget to set:

collectionView.translatesAutoresizingMaskIntoConstraints = false

When setting up the constraints.

Same times the selected cell is not centered after landscape to portrait orientation change. I solve this problem like this:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)


//Scroll to current Item after the orientation change. Sometimes the current item is not centered.
coordinator.animate(alongsideTransition: { context in
self.housesCollectionView.collectionViewLayout.invalidateLayout()
self.housesCollectionView.scrollToItem(at: self.selectedHouseIndexPath, at: .left, animated: true)
})
}