I am trying to make an adaptive layout using a UICollectionView
On an iPhone I would like my view to stack, on an iPad I would like it to arrange 3 in a row.
This is working, however when I rotate my iPad and rotate it back again, my subviews do not return the position they were in originally at in that rotation.
You can see the behaviour if you pay attention to the Recognition item in the gif
This is my view controller
class AdaptiveLayoutCollectionView: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
registerCells()
configureLayout(with: view.bounds.size)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
configureLayout(with: size)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
configureLayout(with: view.bounds.size)
}
final func setLayout(as layout: CollectionViewLayoutOption) {
self.layout = layout
configureLayout(with: view.bounds.size)
}
func registerCells() { /* register cells in super view */ }
private func configureLayout(with size: CGSize) {
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
flowLayout.sectionInset = UIEdgeInsets(top: 8.0, left: 0, bottom: 8.0, right: 0)
if traitCollection.horizontalSizeClass == .regular {
//let minItemWidth: CGFloat = 300
let minItemWidth: CGFloat = size.width / 3
let numberOfCell = size.width / minItemWidth
let width = floor((numberOfCell / floor(numberOfCell)) * minItemWidth)
flowLayout.itemSize = CGSize(width: width, height: 80)
} else {
flowLayout.itemSize = CGSize(width: size.width, height: 80)
}
collectionView.reloadData()
}
}
Related
I have a pager whole screen collectionview. When device rotation, first cell of collectionview is fitting to collectionview on landscape and portrait mode. But when I scroll next cell on collectionview, it is showing two cell. And it is not fitting all screen. How can I fit all cells size to collectionview although I scroll collectionview and rotate. I tried invalideLayout() and many way. But I couldn't achive. Just I only want, collectionviewcell fitted to whole screen in any situation although scroll and rotate.
P.S collectionviewcell size should be whole screen size.
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.current.orientation.isLandscape,
let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let width = view.frame.height
layout.itemSize = CGSize(width: width, height: UIScreen.main.bounds.width)
layout.invalidateLayout()
} else if UIDevice.current.orientation.isPortrait,
let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let width = view.frame.width
layout.itemSize = CGSize(width: width , height: UIScreen.main.bounds.height)
layout.invalidateLayout()
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = view.frame.size.width
return CGSize(width: width , height: UIScreen.main.bounds.height)
}
Make sure to remove spacing between your cells like below. This will resolve your problem.
you have to write your code in viewWillLayoutSubviews(). Like this
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
if UIDevice.current.orientation == .landscapeLeft
{
flowLayout.itemSize = CGSize(width: self.view.frame.width, height: 500)
}
else if UIDevice.current.orientation == .landscapeRight
{
flowLayout.itemSize = CGSize(width: self.view.frame.width, height: 500)
}
flowLayout.invalidateLayout()
}
can you try to Apply following Approach.
1'st Approach
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// YOUR CODE OR FUNCTIONS CALL HERE
}
2' nd Approach
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func rotated() {
if UIDevice.current.orientation.isLandscape,
let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let width = view.frame.height
layout.itemSize = CGSize(width: width, height: UIScreen.main.bounds.width)
layout.invalidateLayout()
} else if UIDevice.current.orientation.isPortrait,
let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
let width = view.frame.width
layout.itemSize = CGSize(width: width , height: UIScreen.main.bounds.height)
layout.invalidateLayout()
}
}
I'm using the following to left align cells in a UICollectionView…
class LeftAlignedFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
for attributes in attributes where attributes.representedElementCategory == .cell {
attributes.frame = layoutAttributesForItem(at: attributes.indexPath).frame
}
return attributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)!
let layoutWidth = collectionView!.frame.width - sectionInset.left - sectionInset.right
// Just left align the first item in a section
if indexPath.item == 0 {
currentItemAttributes.frame.origin.x = sectionInset.left
return currentItemAttributes
}
let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section)
let previousItemFrame = layoutAttributesForItem(at: previousIndexPath).frame
let currentItemFrame = currentItemAttributes.frame
let wholeRowFrameForCurrentItem = CGRect(x: sectionInset.left, y: currentItemFrame.minY, width: layoutWidth, height: currentItemFrame.height)
// Left align the first item on a row
if !previousItemFrame.intersects(wholeRowFrameForCurrentItem) {
currentItemAttributes.frame.origin.x = sectionInset.left
return currentItemAttributes
}
currentItemAttributes.frame.origin.x = previousItemFrame.maxX + minimumInteritemSpacing
return currentItemAttributes, it's
}
}
Building for iOS 11 gives…
But for iOS 12 gives…
In iOS 11, layoutAttributesForElements(in) is called 3 times - the final time with the correct frame sizes. In iOS 12, it's only called twice, with frames being the estimated size.
Per this answer, https://stackoverflow.com/a/52148520/123632 I've tried invalidating the flow layout using the UICollectionView subclass below (and "brute-forcing" in viewDidAppear of the containing view controller), but the result is unchanged.
class LeftAlignedCollectionView: UICollectionView {
private var shouldInvalidateLayout = false
override func layoutSubviews() {
super.layoutSubviews()
if shouldInvalidateLayout {
collectionViewLayout.invalidateLayout()
shouldInvalidateLayout = false
}
}
override func reloadData() {
shouldInvalidateLayout = true
super.reloadData()
}
}
This is a known issue in iOS 12 (see UIKit section here: https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes)
The workaround is to either avoid calling setNeedsUpdateConstraints() or call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:).
I found a solution. Constraining the contentView to the cell fixed it…
override func awakeFromNib() {
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)])
}
I added this in the MyCustomCollectionViewCell file and it worked fine
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return contentView.systemLayoutSizeFitting(CGSize(width: targetSize.width, height: 1))
}
I used collection view to display 10 cells, with portraits:
and on rotation:
I just try code
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let followLayout = demoView.collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return }
followLayout.itemSize = UIScreen.main.bounds.size
followLayout.invalidateLayout()
view.setNeedsLayout()
}
but does not affect.
I just try
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
guard let followLayout = detailView.collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
followLayout.invalidateLayout()
let offset = detailView.collectionView.contentOffset
let width = detailView.collectionView.bounds.size.width
let index = round(offset.x / width)
let newOffset = CGPoint(x: index * size.width, y: offset.y)
detailView.collectionView.setContentOffset(newOffset, animated: false)
coordinator.animate(alongsideTransition: { (context) in
// self.demoView.collectionView.reloadData()
self.detailView.collectionView.setContentOffset(newOffset, animated: false)
}, completion: nil)
}
and this work for me. Thanks for all
I suggest you to center align for every cell
Swift 4
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
let totalCellWidth = CellWidth * CellCount
let totalSpacingWidth = CellSpacing * (CellCount - 1)
let leftInset = (collectionViewWidth - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
let rightInset = leftInset
return UIEdgeInsetsMake(0, leftInset, 0, rightInset)
}
I've created programmatically (without Story Board or Interface Builder) a custom subclass of UITableViewController called FooTableViewController.
In viewDidLoad, I've set tableView.tableHeaderView to an uneditable UITextView with an arbitrary height of 200 and with its text set to a random String.
The UITextView automatically updates its width when I rotate the device. How do I get it to automatically update its height when I set its text (so that it exactly contains its text)?
Note: I'm using an uneditable UITextView instead of a UILabel because I want the user to be able to select and copy text.
Below are the relevant instance methods of FooTableViewController.
// MARK: - UIViewController
override func viewDidLoad() {
super.viewDidLoad()
let textView = UITextView(frame: CGRect.zero, textContainer: nil)
textView.editable = false
textView.font = .systemFontOfSize(17)
textView.scrollEnabled = false
textView.scrollsToTop = false
textView.text = "Random long string"
textView.textContainerInset = UIEdgeInsets(top: 15, left: 12, bottom: 25, right: 12)
tableView.tableHeaderView = textView
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
updateTableHeaderViewHeightForWidth(view.frame.width)
}
// MARK: - UIContentContainer
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
coordinator.animateAlongsideTransition({ _ in self.updateTableHeaderViewHeightForWidth(size.width) }, completion: nil)
}
// MARK: - Helpers
func updateTableHeaderViewHeightForWidth(width: CGFloat) {
let textView = tableView.tableHeaderView!
let oldHeight = textView.frame.height
let newHeight = textView.sizeThatFits(CGSize(width: width, height: CGFloat.max)).height
textView.frame.size.height = newHeight
tableView.contentSize.height += newHeight - oldHeight
}
I'm trying to create a custom calendar, I need to change the spacing of the header labels when the orientation is changed.
I'm using the following code to change the size and spacing between the cells, but how can I do the same for the labels in the header. The header is a custom class.
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
updateCollectionViewLayout(with: size)
}
private func updateCollectionViewLayout(with size: CGSize) {
let itemSizeForPortraitMode : CGSize = CGSize(width: 40, height: 40)
let itemSizeForLandscapeMode: CGSize = CGSize(width: 40, height: 40)
var minimumItemSpacing: CGFloat
if let layout = calendarCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.itemSize = (size.width < size.height) ? itemSizeForPortraitMode : itemSizeForLandscapeMode
minimumItemSpacing = (size.width - (7 * 40)) / 6
layout.minimumInteritemSpacing = minimumItemSpacing
layout.invalidateLayout()
calendarCollectionView.updateConstraints()
}
}
Thanks for help.
I found the solution, I added a function to my header custom class:
override func layoutSubviews() {
super.layoutSubviews()
var labelFrame = CGRect(x: 0.0, y: 43, width: self.bounds.size.width / 7.0, height: 20)
for lbl in self.subviews {
if lbl.tag == 1 || lbl.tag == 2 || lbl.tag == 3 || lbl.tag == 4 || lbl.tag == 5 || lbl.tag == 6 || lbl.tag == 7 {
lbl.frame = labelFrame
labelFrame.origin.x += labelFrame.size.width
}
}
}