This question already has answers here:
Keeping the contentOffset in a UICollectionView while rotating Interface Orientation
(24 answers)
Closed 6 years ago.
I have a paginated, horizontally scrolling UICollectionView where each cell takes up the size of the view/device screen, and one cell appears at a time. I'm experiencing an issue where on device rotation, the cells will transition to the correct new size, but the position of the cells will be off.
Before Rotation:
After Rotation:
On rotation, how can I not only resize the collection view cells properly but ensure that the current cell stays visible?
I've boiled this problem down to a very simple example without a custom flow layout.
Setting the size of the cells to the collectionView frame's size (the collection view is the size of the view controller holding it):
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return collectionView.frame.size
}
Attempting to handle rotation in viewWillLayoutSubviews:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
flowLayout.itemSize = collectionView.frame.size
flowLayout.invalidateLayout()
}
Actually, if I call invalidateLayout() in viewWillLayoutSubviews, setting the itemSize here shouldn't be necessary, as sizeForItemAtIndexPath will be called when the layout is recreated.
I've also tried manually scrolling to the first visible cell during rotation after setting the new itemSize, but that shows no improvement:
if let item = collectionView.indexPathsForVisibleItems().first {
collectionView.scrollToItemAtIndexPath(item, atScrollPosition: .CenteredHorizontally, animated: true)
}
You should handle content offset of your UICollectionView, so in your view controller try to override viewWillTransitionToSize function from UIContentContainer protocol, like this:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
let offset = yourCollectionView?.contentOffset;
let width = yourCollectionView?.bounds.size.width;
let index = round(offset!.x / width!);
let newOffset = CGPointMake(index * size.width, offset!.y);
yourCollectionView?.setContentOffset(newOffset, animated: false)
coordinator.animateAlongsideTransition({ (context) in
yourCollectionView?.reloadData()
yourCollectionView?.setContentOffset(newOffset, animated: false)
}, completion: nil)
}
Related
I have a UICollectionView which is exactly 7 cells wide, with no padding in between. I have done this by adding a flow layout with no interim or line spacing and calculating the width as follows:
public func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
return CGSize(
width: self.frame.width / CGFloat(7),
height: self.frame.height - headerHeight
)
}
This works fantastically within my iPhone app
where rotation isn't enabled. On iPad, when in portrait mode this works fine.
When rotating, firstly the app doesn't redraw, I've attempted to invalidateLayout however this didn't solve the issue. I have to scroll down and I get the following layout
When I tap a cell it does however redraw, but with padding added pushing the 7th cell down onto another row.
I am looking to redraw the cells so all 7 fit on a single row when the app is either rotated or grown and shrunk in split view. How should I handle this correctly?
Add this in your UIViewController class
It will adjust your layout automatically while you change device orientation.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let columnLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
columnLayout.invalidateLayout()
}
I have a UICollectionView with horizontal scrolling and paging. The cells should be the screen size always, so when the orientation of the device changes there is the call collectionView.collectionViewLayout.invalidateLayout() and the item size and collection view offset get recalculated and it works well.
The issue occurs when going to another view controller, then rotating and going back to the view controller with the collection view. Then it looks like the collection view doesn't take into account the orientation change or something like this and there could be two cells visible, which should not happen. Also there is some weird animation when going back to the view controller with the collection view. How should these be fixed?
Here is some code:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
collectionView.collectionViewLayout.invalidateLayout()
coordinator.animate(alongsideTransition: { (context) in
}) { (context) in
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.frame.size
}
func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
let width = collectionView.frame.size.width
let visibleCells = collectionView.visibleCells
if visibleCells.count == 0 {
return CGPoint.zero
}
let cell = visibleCells[0]
let indexPath = collectionView.indexPath(for: cell)
let index = indexPath?.item
let offsetX = CGFloat(index!) * width
let offsetY: CGFloat = 0
let offset = CGPoint(x: offsetX, y: offsetY)
return offset
}
Trying to fix the issue I make the following calls in viewDidAppear but no luck:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
collectionView.collectionViewLayout.invalidateLayout()
collectionView.layoutSubviews()
}
I have a UICollectionView that makes use of one cell per section. It scrolls vertically. I've made the collection view cell size 320X125 although I have played with width to no avail.
When on the iPhone in portrait mode the cell starts at the left of screen which is the desired behavior. However, when I rotate the phone to landscape, the cell centers in the collection view. On the iPad, however, it stays to the left in both orientations.
I would like the cell to stay stuck to the left side of the screen regardless of orientation. How can this be accomplished? While autolayout works for things inside the cell, there does not appear to be an autolayout that can be applied to the cell itself.
I solved the problem by creating a second cell for the iPad. So there is one for iPhone and the other iPad. I then used:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
Based on device orientation I set the preferredMaxLayoutWidth I then used:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
let deviceType = UIDevice.currentDevice().userInterfaceIdiom
if deviceType == .Phone
{
return CGSizeMake(self.view.bounds.width, 170)
}
else
{
return CGSizeMake(self.view.bounds.width, 400)
}
}
This resized the cell based on the phone or tablet.
As a last item, I used the following to redraw after the device was rotated:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
coordinator.animateAlongsideTransition(nil) { (context) -> Void in
self.collectionsView.reloadData()
}
}
I have a UIView inside my UICollectionViewCell that I need to be reproduced. I need this view to be pinned to the left and right of the screen. However, I can't find any way to control the width of the UICollectionViewCell.
Am I just missing something?
You're right. UICollectionViewCell doesn't play by the same AutoLayout rules as other views. Each cell is controlled by a layout class set on the UICollectionView.
Technically, you can drag the handles in Interface Builder and resize that way but it won't be dynamic for different screen widths and orientations.
I can think of two options:
Override the sizeForItemAtIndexPath: in the view controller and set to width of parent container programmatically. See Update 1 on question here for how this is done or check out the Apple documentation here for more background information
Consider using a UITableView if all cells will always be full width
You can use collectionView layouts. This code should help you:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var size = CGSize.zero
if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
size = layout.itemSize;
size.width = collectionView.bounds.width
}
return size
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView.collectionViewLayout.invalidateLayout()
}
I have a headerView in my CollectionView and have resized the headerView programmatically based on the text size of the Label. On rotating to landscape orientation the reusable header view doesn't resize automatically but on scrolling it resizes itself to give the intended result.
Below is the snippet I have used to resize my header.
override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
switch kind{
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "HeaderView", forIndexPath: indexPath) as! CollectionViewHeader
var headerString = westBeaches[indexPath.section] as NSString
var newSize: CGSize = headerString.sizeWithAttributes([NSFontAttributeName:headerView.headerText.font])
headerView.frame.size.width = newSize.width + 20
headerView.layer.cornerRadius = 15
headerView.headerText.text = westBeaches[indexPath.section]
headerView.center.x = collectionView.center.x
headerView.alpha = 0.7
return headerView
default:
assert(false, "Unexpected element Kind")
}
}
CollectionViewHeader is a custom class inheriting UIcollectionReusableView and contains headerText as UILabel. Is there any way to prevent the reusable view from going back to its original size when the orientation changes?
For me I had a UISearchBar as a header, and had this same problem. That is after rotation the search bar doesn't fill the CollectionView width.
I fixed it by animating the search bar's width alongside the rotation animation.
In UIViewController
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
// Animate searchBar too
coordinator.animateAlongsideTransition({ [unowned self] _ in
var frame = self.searchController.searchBar.frame
frame.size.width = size.width
self.searchController.searchBar.frame = frame
}, completion: nil)
}
You have to update UICollectionView flow layout for orientations.
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
collectionView.performBatchUpdates({ () -> Void in
}, completion: { (complete) -> Void in
})
}