Swift: 如何在设备旋转后刷新 UICollectionView 布局
Swift: How to refresh UICollectionView layout after rotation of the device
我使用 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
}}
添加此功能:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myCollection.collectionViewLayout.invalidateLayout()
}
当你改变方向时,这个函数会被调用。
您可以使用
更新您的 UICollectionView 布局
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if isLandscape {
return CGSizeMake(yourLandscapeWidth, yourLandscapeHeight)
}
else {
return CGSizeMake(yourNonLandscapeWidth, yourNonLandscapeHeight)
}
}
更好的选择是调用 invalidateLayout()
而不是 reloadData()
,因为它不会强制重新创建单元格,因此性能会稍微好一些:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
myCollection.collectionViewLayout.invalidateLayout()
}
也可以通过这种方式使其失效
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.collectionView.collectionViewLayout invalidateLayout];
}
要更新 UICollectionViewLayout
,也可以使用 traitCollectionDidChange
方法:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection != nil else { return }
collectionView?.collectionViewLayout.invalidateLayout()
}
viewWillLayoutSubviews() 对我不起作用。 viewDidLayoutSubviews() 也没有。两者都使应用程序进入无限循环,我使用打印命令进行了检查。
其中一种工作方式是
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// Reload here
}
我也遇到了一些问题,但后来解决了:
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)
}
}
我通过在屏幕方向更改时设置通知并重新加载根据屏幕方向设置项目大小的单元格以及将索引路径设置为上一个单元格来解决此问题。这也适用于流程布局。这是我写的代码:
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)
}
调用 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
}
}
我用下面的方法解决了这个问题
override func viewDidLayoutSubviews() {
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
collectionView.collectionViewLayout.invalidateLayout()
collectionView.collectionViewLayout = flowLayout
}
}
当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
}
}
对我有用。这是 Objective-C:
中的代码
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[collectionView.collectionViewLayout invalidateLayout];
}
请谅解
traitCollectionDidChange(previousTraitCollection :) 在 iPad 旋转时不会被调用,因为大小 class 在纵向和横向方向上都是 .regular。只要集合视图大小发生变化,就会调用 viewWillTransition(to:with:)。
此外,如果您的应用支持多任务处理,则不应使用 UIScreen.mainScreen().bounds,因为它可能不会占据整个屏幕,最好使用 collectionView.frame.width。
我的代码:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.collectionView.collectionViewLayout.invalidateLayout()
}
会正常工作!!!
试试这个:
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
}
}
文档:
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.
除了其他人已经提到的之外,如果您自己创建集合视图而不是继承 UICollectionViewController,请不要忘记设置:
collectionView.translatesAutoresizingMaskIntoConstraints = false
设置约束时。
我使用 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
}}
添加此功能:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myCollection.collectionViewLayout.invalidateLayout()
}
当你改变方向时,这个函数会被调用。
您可以使用
更新您的 UICollectionView 布局func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if isLandscape {
return CGSizeMake(yourLandscapeWidth, yourLandscapeHeight)
}
else {
return CGSizeMake(yourNonLandscapeWidth, yourNonLandscapeHeight)
}
}
更好的选择是调用 invalidateLayout()
而不是 reloadData()
,因为它不会强制重新创建单元格,因此性能会稍微好一些:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
myCollection.collectionViewLayout.invalidateLayout()
}
也可以通过这种方式使其失效
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.collectionView.collectionViewLayout invalidateLayout];
}
要更新 UICollectionViewLayout
,也可以使用 traitCollectionDidChange
方法:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection != nil else { return }
collectionView?.collectionViewLayout.invalidateLayout()
}
viewWillLayoutSubviews() 对我不起作用。 viewDidLayoutSubviews() 也没有。两者都使应用程序进入无限循环,我使用打印命令进行了检查。
其中一种工作方式是
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// Reload here
}
我也遇到了一些问题,但后来解决了:
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)
}
}
我通过在屏幕方向更改时设置通知并重新加载根据屏幕方向设置项目大小的单元格以及将索引路径设置为上一个单元格来解决此问题。这也适用于流程布局。这是我写的代码:
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)
}
调用 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
}
}
我用下面的方法解决了这个问题
override func viewDidLayoutSubviews() {
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
collectionView.collectionViewLayout.invalidateLayout()
collectionView.collectionViewLayout = flowLayout
}
}
当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
}
}
对我有用。这是 Objective-C:
中的代码- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[collectionView.collectionViewLayout invalidateLayout];
}
请谅解
traitCollectionDidChange(previousTraitCollection :) 在 iPad 旋转时不会被调用,因为大小 class 在纵向和横向方向上都是 .regular。只要集合视图大小发生变化,就会调用 viewWillTransition(to:with:)。
此外,如果您的应用支持多任务处理,则不应使用 UIScreen.mainScreen().bounds,因为它可能不会占据整个屏幕,最好使用 collectionView.frame.width。
我的代码:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.collectionView.collectionViewLayout.invalidateLayout()
}
会正常工作!!!
试试这个:
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
}
}
文档:
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.
除了其他人已经提到的之外,如果您自己创建集合视图而不是继承 UICollectionViewController,请不要忘记设置:
collectionView.translatesAutoresizingMaskIntoConstraints = false
设置约束时。