I have a collection view that displays a detailed view of an article with dynamic content size.
I have multiple items in the collection view that can be scrolled horizontally.
When the horizontal flow layout is active, the dynamic cell cannot be scrolled vertically and the content gets hidden under the view.
If I disable the horizontal flow layout, I can scroll the content vertically. But, all the other items gets stacked on the bottom of each cell.
I am basically looking for a news portal like layout, where a user can scroll through the content of the article vertically, and also scroll horizontally through the list of articles.
I have setup my collection view this way.
lazy var blogDetailCollectionView: UICollectionView =
{
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = 0
//layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
let blogDetailCollection = UICollectionView(frame: .zero, collectionViewLayout: layout)
blogDetailCollection.delegate = self
blogDetailCollection.dataSource = self
blogDetailCollection.backgroundColor = .white
blogDetailCollection.isPagingEnabled = true
blogDetailCollection.translatesAutoresizingMaskIntoConstraints = false
return blogDetailCollection
}()
The collection view cell has a dynamic height that needs to be vertically scrollable, which I have set here.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
guard let blogDescription = blogContainer?.blogs[indexPath.item].blogDescription else {return CGSize(width: frame.width, height: frame.height)}
let widthOfTextView = frame.width
let size = CGSize(width: widthOfTextView, height: 1000)
let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: UIFont.labelFontSize)]
let estimatedFrame = NSString(string: blogDescription).boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
let blogImageHeight = frame.width * (2 / 3)
return CGSize(width: widthOfTextView, height: estimatedFrame.height + blogImageHeight)
}
The collection view has multiple items, which needs to be scrolled horizontally.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return (blogContainer?.blogs.count)!
}
Have you tried.Setting the collectionView
let layout:UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: screenWidth/2,height: 200)
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1
collectionView.setCollectionViewLayout(layout, animated: true)
And set the collection view for both horizontal or vertical scroll when the button tapped declare the layout globally above viewDidLoad().
Related
I have a collection view where 2 cells are being placed horizontally in portrait. In landscape mode I want 5 cells to be placed accordingly.
There is a method setItemSizeForCollectionView that defines my layout based on a device orientation.
It gets called in method configureCollectionView() which is called in viewDidLoad() and in viewWillTransition() when device gets rotated.
Views are created programmatically, so the size of the main view and its safeAreaInsets are not available in viewDidLoad.
I was thinking about subtracting those insets from the device size based on the orientation.
Any hint how can I achieve proper layout?
private func setItemSizeForCollectionView(layout: UICollectionViewLayout, with size: CGSize) {
guard let layout = layout as? UICollectionViewFlowLayout else { return }
var numberOfElementsHorizontally: CGFloat
if UIDevice.current.orientation.isPortrait {
numberOfElementsHorizontally = 2
layout.itemSize = CGSize(width: size.width / numberOfElementsHorizontally,
height: size.width / numberOfElementsHorizontally)
}
if UIDevice.current.orientation.isLandscape {
numberOfElementsHorizontally = 5
layout.itemSize = CGSize(width: size.width / numberOfElementsHorizontally,
height: size.width / numberOfElementsHorizontally)
}
}
private func configureCollectionView() {
let layout = UICollectionViewFlowLayout()
let width = UIScreen.main.bounds.width
let height = UIScreen.main.bounds.height
setItemSizeForCollectionView(layout: layout, with: CGSize(width: width, height: height))
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.scrollDirection = .vertical
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(CharacterCell.self, forCellWithReuseIdentifier: CharacterCell.reuseIdentifier)
}
This is a general purpose method for selecting how many cells per row you want in a collectionView:
func sizeForCell(itemsPerRow: Int)
{
return CGSize( width: bounds.width - (minimumInteritemSpacing * itemsPerRow) / itemsPerRow, height: 200)
}
EDIT: A bit more explanation, what we're doing here is setting the cell width to the collectionView's width, then dividing it by however many cells we want in the row, then removing some more width to leave for padding (number of items * padding amount).
This is what I have:
A Collection View with 2 columns with each an equal distance apart.
Each cell loads SmallCardView.xib. The SmallCardView contains a square image with some text below.
The problem:
I want the width of the view to match that of it's parent (the cell). This is best illustrated by comparing screen sizes
As you can see above, the cell (purple outline) sizes correctly on both screens but the SmallCardView remains the same size
Here is the code in my Collection View Controller:
viewDidLoad -
private let spacing: CGFloat = 20.0
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
self.collectionView?.collectionViewLayout = layout
}
sizeForItemAt -
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let numberOfItemsPerRow: CGFloat = 2
let spacingBetweenCells: CGFloat = 20
let totalSpacing = (2 * self.spacing) + ((numberOfItemsPerRow - 1) * spacingBetweenCells) // Amount of total spacing in a row
if let collection = self.collectionView {
let width = (collection.bounds.width - totalSpacing)/numberOfItemsPerRow
return CGSize(width: width, height: width * 1.2)
} else {
return CGSize(width: 0, height: 0)
}
}
Thanks!
You can embed view with constraints to edges as:
extension UIView {
func makeEdges(to view: UIView, useMargins: Bool = false) -> [NSLayoutConstraint] {
return [
(useMargins ? layoutMarginsGuide.leftAnchor : leftAnchor).constraint(equalTo: view.leftAnchor),
(useMargins ? layoutMarginsGuide.rightAnchor : rightAnchor).constraint(equalTo: view.rightAnchor),
(useMargins ? layoutMarginsGuide.topAnchor : topAnchor).constraint(equalTo: view.topAnchor),
(useMargins ? layoutMarginsGuide.bottomAnchor : bottomAnchor).constraint(equalTo: view.bottomAnchor)
]
}
func edges(to view: UIView, useMargins: Bool = false) {
NSLayoutConstraint.activate(makeEdges(to: view, useMargins: useMargins))
}
}
And use it as:
let cardView = ...
cardView.translatesAutoresizingMaskIntoConstraints = false
let cell = ...
cell.contentView.addSubview(cardView)
cell.contentView.edges(to: cardView, useMargins: true)
First take collectionview from storyboard. Set it's constraints like as below :-
Leading space to container - 20
Trailing space to container - 20
Top space to container - 20
Bottom space to container - 20
Then select collectionview and remove lines padding that is default 10. So, update with 20. So, cellpadding should be 10 each side.
Then Go to the viewcontroller file and add all 3 delegates
1. UICollectionViewDelegate
2. UICollectionViewDataSource
3. UICollectionViewDelegateFlowLayout
Then add following code in your swift file :-
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (self.view.frame.size.width - 60) / 2, height: (self.view.frame.size.width - 60) / 2)
}
I am using this code
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let padding: CGFloat = 50
let collectionViewSize = collectionView.frame.size.width - padding
return CGSize(width: collectionViewSize/2, height: collectionViewSize/2)
}
I am able to get a 2 column collection view on all iPhones except iPhone X and iphone XR, I don't know why
How to force 2 columns for all iPhones?
You can set layout of your collectionView by creating new layout and set it's itemSize, minimumInteritemSpacing and minimumLineSpacing and then assign new layout as collectionView.collectionViewLayout:
func setCollectionViewLayout(withPadding padding: CGFloat) {
let layout = UICollectionViewFlowLayout()
let size = (collectionView.frame.width - padding) / 2
layout.itemSize = CGSize(width: size, height: size)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView.collectionViewLayout = layout
}
and then call this method in viewDidLayoutSubviews (this is moment when frames are loaded and you can calculate with collectionView's frame)
override func viewDidLayoutSubviews() {
setCollectionViewLayout(withPadding: 50)
}
Note: I would recommend you to set leading and trailing constraints of collectionView to constant 25 instead of using padding
I suggest that you calculate width according to safeAreaLayoytGuide and, if you're using UICollectionViewFlowLayout, sectionInset. For UICollectionViewFlowLayout the following code will calculate proper width:
let sectionInset = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset
let width = collectionView.safeAreaLayoutGuide.layoutFrame.width
- sectionInset.left
- sectionInset.right
- collectionView.contentInset.left
- collectionView.contentInset.right
If you need two columns, than item width will be calculated like that:
let space: CGFloat = 10.0
let itemSize = CGSize(width: (width - space) / 2, height: 100 /*DESIRED HEIGHT*/)
I have a collection view that is going to display user skills.
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .white
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.keyboardDismissMode = .interactive
collectionView.delegate = self
collectionView.isScrollEnabled = false
collectionView.dataSource = self
collectionView.register(cvcSkills.self, forCellWithReuseIdentifier: self.skillsId)
return collectionView
}()
Each skill is in a cell that has a width set depending on the length of the word (as shown in the image)
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var width:CGFloat = 0
// Width size calculated depending on the size of the text
let skill = skills[indexPath.row]
if let name = skill.name {
let size: CGSize = name.size(withAttributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14.0)])
width = size.width + 30
}
}
return CGSize(width: width, height: 40)
}
As you can see the spacing between these cells are arbitrary for some reason and i cant seem to get them to have an equal amount of distance from eachother.
I have tried setting insets like so(this is my current code which is displayed in the image):
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsetsMake(0, 5, 0, 5)
layout.invalidateLayout()
collectionView.collectionViewLayout = layout
I have also tried setting minimumInteritemSpacing and minimumLineSpacing to diffrent values but i still keep getting diffrent size spaces between each of the cells.
Any idea on how i could get these cells to be spaced 10 away from eachothere.
As Fogmeister suggested, you need to create your own custom flow layout to support this need.
However, if you need, I have a flow layout that I have created that supports the above requirement, i.e., try vertical type (there are other types as well).
https://github.com/varunpm1/VPCollectionViewLayout
Although I shared it, I would recommend you to go through the code and try it yourself. Working with custom flow layout is fun.
Note, I have scoured the internet and have not found a place to both size and centers cells that works. I tried doing it myself but I keep running to bugs I can't avoid. I am new to Swift. My code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath:IndexPath) -> CGSize {
let cellWidth = collectionView.frame.size.width / 7.0
let cellHeight = collectionView.frame.height - 4.0
let imageSideLength = cellWidth < cellHeight ? cellWidth : cellHeight
return CGSize(width: imageSideLength, height: imageSideLength)
}
//centers the cells
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
// Make sure that the number of items is worth the computing effort.
guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout,
let dataSourceCount = photoCollectionView.dataSource?.collectionView(photoCollectionView, numberOfItemsInSection: section),
dataSourceCount > 0 else {
return .zero
}
let cellCount = CGFloat(dataSourceCount)
let itemSpacing = flowLayout.minimumInteritemSpacing
let cellWidth = flowLayout.itemSize.width + itemSpacing
let cellHeight = flowLayout.itemSize.height
var insets = flowLayout.sectionInset
// Make sure to remove the last item spacing or it will
// miscalculate the actual total width.
let totalCellWidth = (cellWidth * cellCount) - itemSpacing
let contentWidth = collectionView.frame.size.width - collectionView.contentInset.left - collectionView.contentInset.right
let contentHeight = collectionView.frame.size.height
// If the number of cells that exist take up less room than the
// collection view width, then center the content with the appropriate insets.
// Otherwise return the default layout inset.
guard totalCellWidth < contentWidth else {
return insets
}
// Calculate the right amount of padding to center the cells.
let padding = (contentWidth - totalCellWidth) / 2.0
insets.left = padding
insets.right = padding
insets.top = (contentHeight - cellHeight) / 2.0
//insets.bottom = (contentHeight - cellHeight) / 2.0
return insets
}
}
I try to use two separate functions: the first to size the cells and the second to center the cells. (Note I only want new cells to expand horizontally, with a maximum of 6 cells.) However, my calculation of cell height and width in the 2nd function does not agree with how I set it in the first function, setting off a chain of issues. Any insight on how to both size and center the cells such that I can have 1-6 cells horizontally fit on my screen centered would be great.
Your layout calls are conflicting. Try following THIS Tutorial to get the hang of it.
Otherwise a good answer for this is HERE
var flowLayout: UICollectionViewFlowLayout {
let _flowLayout = UICollectionViewFlowLayout()
// edit properties here
_flowLayout.itemSize = CGSize(width: 98, height: 134)
_flowLayout.sectionInset = UIEdgeInsetsMake(0, 5, 0, 5)
_flowLayout.scrollDirection = UICollectionViewScrollDirection.horizontal
_flowLayout.minimumInteritemSpacing = 0.0
// edit properties here
return _flowLayout
}
Set it with:
self.collectionView.collectionViewLayout = flowLayout // after initializing it another way
// or
UICollectionView(frame: .zero, collectionViewLayout: flowLayout)