2 column collectionview sizing problem in some iphones - ios

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*/)

Related

Number of elements in collection view in landscape mode

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

Swift - Facebook style image grid using UICollectionViewFlowLayout

I'm trying to implement Facebook style image layout in Swift. I found this SO question and followed the approach suggested by one of the answers. As a result, I almost achieved what I wanted to.
As you can see in the image above, however, there's an overflow: the three smaller cells seems to be taking up more than the screen width. However, if I subtract 0.1 to each of the cell's width, the problem seems to have been solved.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let item = indexPath.item
let width = collectionView.bounds.size.width
let padding:CGFloat = 5
if item < 2 {
let itemWidth:CGFloat = (width - padding) / 2.0
return CGSize(width: itemWidth, height: itemWidth)
} else {
let itemWidth:CGFloat = (width - 2 * padding) / 3.0
// let itemWidth:CGFloat = (width - 2 * padding) / 3.0 - 0.1 // This WORKS!!
return CGSize(width: itemWidth, height: itemWidth)
}
}
I guess the overflow is caused by the value of itemWidth rounding up. But I feel like subtracting a random hard-coded value is not the best practice when dealing with this kind of issue.
Can anyone suggest a better approach for this?
Below is the full code that is reproducible.
class ContentSizeCollectionView: UICollectionView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
class GridVC: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, UICollectionViewDataSource {
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 5
layout.minimumInteritemSpacing = 5
let collectionView = ContentSizeCollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "ident")
return collectionView
}()
override func viewDidLoad() {
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let item = indexPath.item
let width = collectionView.bounds.size.width
let padding:CGFloat = 5
if item < 2 {
let itemWidth:CGFloat = (width - padding) / 2.0
return CGSize(width: itemWidth, height: itemWidth)
} else {
let itemWidth:CGFloat = (width - 2 * padding) / 3.0
return CGSize(width: itemWidth, height: itemWidth)
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ident", for: indexPath)
cell.backgroundColor = .red
return cell
}
}
Update
floor((width - 2 * padding) / 3.0) did prevent the overflow, but it leaves a small gap at the end.
Change this line:
let itemWidth:CGFloat = (width - 2 * padding) / 3.0
to this:
let itemWidth:CGFloat = floor((width - 2 * padding) / 3.0)
You may want to do that with a 2 item row as well.
Edit
The initial problem is that floating-point numbers are approximations.
So, on an iPhone 8, for example, the view width is 375 points. The cell width calculation of:
let padding: CGFloat = 5
let itemWidth:CGFloat = (width - 2 * padding) / 3.0
itemWidth ends up being 121.666... (repeating), which UIKit interprets as 121.66666666666667.
So, 121.66666666666667 * 3.0 + 10 equals (roughly) 375.00000000000011 and... that is greater than 375.0.
So, the collection view says "can't fit that on one row."
Using floor() fixes the problem, except... it hits a really weird bug!
If you change numberOfItemsInSection from 5 to 8, you'll get two rows of 3, and there will be no gap on the right.
We can get around this by making the side cells slightly narrower than the center cell like this:
// we want all three to be the same heights
// 1/3 of (width - 2 * padding)
let itemHeight: CGFloat = (width - 2 * padding) / 3.0
// left and right cells will be 1/3 of (width - 2 * padding)
// rounded down to a whole number
let sideW: CGFloat = floor(itemHeight)
// center cell needs to be
// full-width minus 2 * padding
// minus
// side-width * 2
let centerW: CGFloat = (width - 2 * padding) - sideW * 2
// is it left (0), center (1) or right (2)
let n = (item - 2) % 3
// use the proper width value
let itemWidth: CGFloat = n == 1 ? centerW : sideW
return CGSize(width: itemWidth, height: itemHeight)
Or, what seems to be working is making the width just slightly smaller than the floating-point 1/3rd. You used -0.1, but it also works with:
let itemWidth:CGFloat = ((width - 2 * padding) / 3.0) - 0.0000001
In any case, that hopefully explains the reason for the "2 cells instead of 3" issue, and two options for avoiding it.

Swift -how to make a smaller version of a 16:9 video thumbnail but keep the aspect ratio the same

The aspect ratio for a 1080p hd video is 16:9 and to set a frame for the video in a cell so that it would fill it up completely I would use view.frame.width * 9 / 16:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// the collection view is pinned to both sides of the vc with no spacing
let width = collectionView.frame.width
let videoHeight: CGFloat = width * 9 / 16
return CGSize(width: width, height: videoHeight)
}
// inside the cell itself:
let videoHeight: CGFloat = self.frame.width * 9 / 16
contentView.addSubview(thumbnailImageView)
thumbnailImageView.topAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
thumbnailImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
thumbnailImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
// here is where I set the thumbnail's height
thumbnailImageView.heightAnchor.constraint(equalToConstant: videoHeight).isActive = true
This perfectly gives me:
The thing is I want a smaller version of the thumbnailImageView similar to youtube. I want to add the smaller thumbnailImageView on the left side of the screen but keep the aspect ratio the same.
The problem is when I attempted to do so I got a square instead of a rectangle. I divided the width of the cell by 3 and then multiplied it by 9 /16 but that isn't working. I used (width / 3) because I though it would keep the aspect ratio the same but reduce the size of the thumbnailImageView but it didn't work.
Where am I going wrong at?
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// the collection view is pinned to both sides of the vc with no spacing
let width = collectionView.frame.width
let videoHeight: CGFloat = (width / 3) * 9 / 16
return CGSize(width: width, height: videoHeight)
}
// inside the cell itself
let videoHeight = (self.frame.width / 3) * 9 / 16
contentView.addSubview(thumbnailImageView)
thumbnailImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8).isActive = true
thumbnailImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true
// here is where I set the thumbnail's height and width
thumbnailImageView.heightAnchor.constraint(equalToConstant: videoHeight).isActive = true
thumbnailImageView.widthAnchor.constraint(equalToConstant: videoHeight).isActive = true
You can use AVMakeRect(aspectRatio:insideRect:) function to get a scaled rectangle that maintains the specified aspect ratio within a bounding rectangle.
You can refer the API here.
By using the above API, your videoHeight value would be populated like below.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// the collection view is pinned to both sides of the vc with no spacing
let width = collectionView.frame.width
let rect = AVMakeRect(aspectRatio: CGSize(width: 16, height: 9), insideRect: CGRect(origin: .zero, size: CGSize(width: width / 3, height: CGFloat.infinity)))
let videoHeight: CGFloat = rect.size.height
return CGSize(width: width, height: videoHeight)
}
And in cell, you can simply have height as follows.
let videoHeight = self.frame.height

How to properly size and center expanding cells in UICollectionView Swift 4.0

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)

UICollectionView with zero cell spacing

I've followed this answer and try to implement 5 cells per row and it's working great when I check on iPhone 6 & iPhone SE as below.
But the problem occures when I try to run it on iPhone 6 Plus. Can anyone help me out on figuring out the issue please?
This is my code.
screenSize = UIScreen.main.bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
let itemWidth : CGFloat = (screenWidth / 5)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.sectionInset = UIEdgeInsets(top: 10.0, left: 0, bottom: 10.0, right: 0)
collectionView!.collectionViewLayout = layout
It has to be
let itemWidth : CGFloat = (screenWidth / 5.0)
So the result will not be rounded.
Updated
Please make sure if you use storyboard to create your UICollectionView, remember to set autolayout to the collection view's size so that it is updated to whatever current screen size is.
Update 2
If you use storyboard there is no need to create a UICollectionViewFlowLayout. You can set the insets and spacings from storyboard.
Then in your .m file implement this to determine item's size.
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return yourDesiredSize;
}
To fix the blank spaces the size of each cell should be a round number. Then the difference between the sum of rounded numbers and the size of collectionView can be equally distributed or put in one cell.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// To avoid white space inbetween cells we're rounding the width of each cell
var width = CGFloat(floorf(Float(screenWidth/CGFloat(objects.count))))
if indexPath.row == 0 {
// Because we're rounding the width of each cell the cells don't cover the space completly, so we're making the first cell a few pixels wider to make sure we fill everything
width = screenWidth - CGFloat(objects.count-1)*width
}
return CGSize(width: width, height: collectionView.bounds.height)
}

Resources