I recently implemented a collection view in a app, the only problem I'm experiencing is that the collection view goes from 2 column to 1 column on smaller devices with a lot of padding on both sides
heres on a display smaller then a iPhone 6:
and heres how it looks on a display bigger or equal to a iPhone 6:
I did try several method where if the display width was smaller then a certain number it would scale up the cells, but it failed to work out because the cells where indeed scaled up but where not centered and over-lapping themselves.
You need to calculate the cell width.Try this code.
class YourClass: UIViewController {
//MARK:-Outlets
#IBOutlet weak var yourCollectionView: UICollectionView!
//Mark:-Variables
var cellWidth:CGFloat = 0
var cellHeight:CGFloat = 0
var spacing:CGFloat = 12
var numberOfColumn:CGFloat = 2
//MARK:-LifeCycle
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
yourCollectionView.contentInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
if let flowLayout = yourCollectionView.collectionViewLayout as? UICollectionViewFlowLayout{
cellWidth = (yourCollectionView.frame.width - (numberOfColumn + 1)*spacing)/numberOfColumn
cellHeight = cellWidth //yourCellHeight = cellWidth if u want square cell
flowLayout.minimumLineSpacing = spacing
flowLayout.minimumInteritemSpacing = spacing
}
}
extension YourClass:UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: cellWidth, height: cellHeight)
}
}
You need to calculate your sizes depending on the phone size - if two cell's width are larger then screen size ( including all offsets between them ) they will layout as in the first picture.
You have two options:
One is to deal with sizes and rescale cells according to device size
Two leave it as is - Make small changes for iphone6/6s/7.
If you opt out for first one, you will need to set up constraint for these sizes - aspect ratio or center horizontally ( which could crop the image a little ).
For changing sizes dynamically, take a look at:
https://developer.apple.com/reference/uikit/uicollectionviewdelegateflowlayout
More specific:
https://developer.apple.com/reference/uikit/uicollectionviewdelegateflowlayout/1617708-collectionview
Related
I need to make UICollectionView cells in oval shape where height is fixed but width is dynamic and it has a limit also, if text longer than that, then text should scroll. Any third party option available for this or need to create own using UICollectionView. Please guide.
Below is the image what i am trying to achieve. I want to know before starting should i look for third parties or use UICollectionView to make own. I have short time to complete that's why to avoid time on searching asking in starting itself which direction to follow.Please guide.
You can use a UICollectionViewFlowLayout and Auto Layout to achieve this.
Create a UICollectionViewCell with a container view.
Pin this container view the edges of the cell with auto layout
Add a UILabel to this container view and pin it to all edges of the container view (give it a background color to distinguish from the cell background)
In the UICollectionViewCell subclass you'll want to round the corners of the container view, e.g. self.containerView.layer.cornerRadius = self.containerView.height / 2
In the UICollectionViewFlowLayoutDelegate method, estimatedSizeForItem return an approximate size for the cell (auto layout will calculate the actual size.)
The important thing to remember is your cell needs to have enough constraints so that the auto layout engine can calculate the actual height and width based on the content.
Edit: If you want a fixed height, ensure your label can only have a single line. Or add a height constraint.
Finally, i found a library TagListView that can be installed through cocoapods with lots of customisation and swift 4 support also.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let nw = intersts[indexPath.row]
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let size = CGSize(width: 250, height: 1500)
let estimatedFrame = NSString(string: nw).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font : UIFont.systemFont(ofSize: 17)], context: nil)
let attributes = [NSAttributedStringKey.font : UIFont.systemFont(ofSize: 17)]
let yourLabelSize: CGSize = nw.size(withAttributes:attributes )
var width1 = yourLabelSize.width + 30
if width1 < 30 {
width1 = 30
}
return CGSize(width: estimatedFrame.width+20, height: estimatedFrame.height+20)
}
I just extend or implement #Tim answer. So after you build the cell as described in his answer, then specify the cell width and height to be flexible using the sample code below i.e similar to what he described
let collectionViewLayout = UICollectionViewFlowLayout()
collectionViewLayout.scrollDirection = .horizontal
let itemWidth = Constants.myCollectionViewItemWidth
let itemHeight = Constants.myCollectionViewItemHeight
collectionViewLayout.estimatedItemSize = CGSize(width: itemWidth, height: itemHeight)
myCollectionView.collectionViewLayout = collectionViewLayout
I'm trying to create an UI like below.
For this purpose I'm using UICollectionView and FlowLayout.
For showing the first cell with full width and remaining cell as 3 column, I've implemented the sizeForItemAtIndexPath: and minimumInteritemSpacingForSectionAtIndex methods:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
var cellSize = CGSizeZero
if indexPath.item == 0
{
let width = self.cvMedia.frame.width - 20
let height = (width * 9)/16
cellSize = CGSize(width: width, height: height)
}
else
{
let width = self.cvMedia.frame.width / 3 - 20
let height = (width * 16)/9
cellSize = CGSize(width: width, height: height)
}
return cellSize
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat
{
return 20.0
}
But I'm getting the following output in iPhone 6SPlus and iPhone 6Plus:
I tried by changing the item spacing from 20 to 19 and it worked on simulator, but on actual device it still shows the same behaviour. If I change the spacing values I'm getting the correct output on some device versions, and not working on some versions. I can add a device version check and based on that I can return the value. But it is not an elegant solution and it will break on future versions. Can anyone help me to solve this issue ? Thanks in advance.
You should use the collection view's width as your parameter when making this layout.
For instance,
You need 3 cells to fit the view.
The cells must have 20pt spacing in between them.
Solution:
Calculate the size of the cell at runtime either on viewWillAppear() or cellForRowAtIndexPath()
Scenario:
Width of device is 320pt. Collectionview's width is 300pt( spacing 10pt L & R) Spacing : 20pt between cells. Cell's needed 3!!
Cell Size: ?
Now..Start calculating in either method.
Cell Spacing = (3-1) * 20 = 2 * 20 = 40pt
Remaining collection view width = 300pt - 40pt = 260pt
Cell size = 260pt/3 = 86.667 pt. (86pt apprx).
In Code:
cellWidth = ( collectionView?.bounds.width - minimumSpacing * (numberOfCellsInARow - 1 ) ) / numberOfCellsInARow
Also, this only gives the cellWidth. You need cellHeight as well.
You might already have an aspect ratio for the cell. Using that, calculate the cellHeight as well.
Kind Regards,
Suman Adhikari
Check out this and change the values for min spacing to 0 for both cells and lines
In my app I am using using UICollectionView. I want to set fix space between UICollectionViewCell. so how can I do this?
here is my code:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 20
}
by this line I can set space between cell?
here is my screenshot please see this. and let me know how can i set fix distance in both landscape or portrait mode
Maybe,you need implementation follow method.
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
You can adjust the spacing between UICollectionCell by using the storyboard Min. spacing property of UICollectionView.
Here you have to set Min spacing value for cells and lines.
Hope it would help you.
You can also manage the spacing using the size inspector of Collection View
http://i.stack.imgur.com/Nvp3g.png
If you want exact spacing between the cells you need to calculate the size for the cells that will best fit to allow for the spacing you need without wrapping while considering the sectionInsets and CollectionView bounds. eg.
let desiredSpacing: CGFloat = 20.0
if let layout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
layout.minimumLineSpacing = desiredSpacing
let totalCellsContentWidth = self.collectionView!.bounds.width - layout.sectionInset.left - layout.sectionInset.right
let numberOfCellsPerRow: CGFloat = 10
let numberOfSpacesPerRow = numberOfCellsPerRow - 1
let cellWidth = (totalCellsContentWidth - (numberOfSpacesPerRow * desiredSpacing) / numberOfCellsPerRow)
layout.itemSize = CGSize(width: cellWidth, height: cellWidth)
}
Assuming your cells are the same size the minimumLineSpacing is simple but would otherwise expand to fit the largest cell on the row before wrapping to the next line. As you can see it's the cell spacing that is a bit more complicated.
I am trying to achieve a custom layout like this one :
I am trying to implement it via a UICollectionView. First I use this code to have the desired size :
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize{
return CGSizeMake((collectionView.frame.size.width / 2) - 2 , (collectionView.frame.size.height / 3) - 2)
}
It's working fine.
The problem is that my picture is not properly centered. I did it this way :
Let me explain :
One constraint to align the center of the uiimage on X
One constraint to do the same thing on Y
One constraint to keep the image ratio
One constrain to say that the height of the image is 70% of the cell height
And the result is very not the one expected :
i did it using custom layout.
let collLayout:UICollectionViewFlowLayout = layout as! UICollectionViewFlowLayout
collLayout.scrollDirection = .Vertical
collLayout.minimumInteritemSpacing = 10
let width = (frame.size.width - 3*collLayout.minimumInteritemSpacing)*0.5
collLayout.itemSize = CGSizeMake(width, width)
collLayout.sectionInset = UIEdgeInsetsMake(0, 10, 0, 10)
And then initialize your collection view with this custom layout.
How can I get a pixel perfect one (1) pixel with border line in a UICollectionView (e.g. to make a month calendar). The issue is that where cells meet, their borders meet, so it is effectively 2 pixels (not 1 pixel).
Therefore this is a CollectionView layout related issue/question. As the way each cell puts it own 1 pixel border gives rise, when the cells all meet up with zero spacing, to what looks like a 2 pixels border overall (except for outer edges of the collectionView which would be 1 pixel)
Seeking what approach/code to use to solve this.
Here is the custom UICollectionViewCell class I use, and I put borders on the cells in here using drawrect.
import UIKit
class GCCalendarCell: UICollectionViewCell {
#IBOutlet weak var title : UITextField!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override func drawRect(rect: CGRect) {
self.layer.borderWidth = 1
self.layer.borderColor = UIColor.redColor().CGColor
}
}
There are a few strategies you can take to solve this issue. Which is best depends on some of the details of your layout.
Option 1: Half Borders
Draw borders of 1/2 the desired width around each item. This has been suggested. It achieves the correct width for borders between items, but the borders around the edge of the collection will also be half the desired width. To solve this you can draw a border around the entire collection, or sections.
If you're month layout only renders days in a given month, meaning each month has a unique non-rectangular shape, fixing the edge borders is more complicated than it's worth.
Option 2: Overlapping Cells
Draw the full border around each item but adjust the frames so they overlap by half of the width of the borders, hiding the redundancy.
This is the simplest strategy and works well, but be aware of a few things:
Be mindful that the outer edges of the layout are also slightly changed. Compensate for this if necessary.
The borders will overlap, so you can't use semi-transparent borders.
If not all the borders are the same color, some edges will be missing. If there is just a single cell with a different border, you can adjust the item's z-index to bring it on top of the other cells.
Option 3: Manual Drawing
Rather than using borders on the cell at all, you can add a border around the container and then manually draw the borders in the superview. This could be accomplished by overriding drawRect(_:) and using UIBezierPaths to draw horizontal and vertical lines at the intervals of the cells. This is the most manual option. It gives you more control, but is much more work.
Option 4: Draw Only Some Borders in Each Cell
Rather than assigning the layer's border width and border color, which draws a border around the entire view, selectively draw edges of cells. For example: each cell could only draw it's bottom and right edge, unless there is no cell above or to the right of it.
This can again lead to complications if you have an irregular grid. It also requires each cell to know about it's context in the grid which requires more structure. If you go this route, I would suggest subclassing UICollectionViewLayoutAttributes and adding additional information about which edges should be drawn. The edge drawing itself can again be done by creating UIBezierPath's in drawRect(_:) for each edge.
I usually divide the border width by the screen scale:
width = 1.0f / [UIScreen mainScreen].scale
Use nativeScale!
CGFloat width = 1.0f / [UIScreen mainScreen].nativeScale;
With respect to the very thorough accepted answer, I don't think any of those methods are very clean. To make this look good in all scenarios you're going to want to draw the grid "manually" but doing so in a drawRect isn't a good idea a) because this will draw under your cells and b) drawRect paints the screen 60 times a second so you will encounter performance issues with a large scrolling view.
Better IMO is to use CALayers. With CALayer you can drop a grid line onto the view once and it stays on the GPU from then on so even a huge grid can draw at 60fps. I also find the implementation easier and more flexible. Here's a working example:
class GridLineCollectionView: UICollectionView {
private var gridLineContainer: CALayer!
private var verticalLines = [CALayer]()
private var horizontalLiines = [CALayer]()
private var cols: Int {
return Int(self.contentSize.width / self.itemSize.width)
}
private var rows: Int {
return Int(self.contentSize.height / self.itemSize.height)
}
private var colWidth: CGFloat {
return self.itemSize.width + self.sectionInsets.left + self.sectionInsets.right
}
private var rowHeight: CGFloat {
return self.itemSize.height + self.sectionInsets.top + self.sectionInsets.bottom
}
private var itemSize: CGSize {
return (self.collectionViewLayout as! UICollectionViewFlowLayout).itemSize
}
private var sectionInsets: UIEdgeInsets {
return (self.collectionViewLayout as! UICollectionViewFlowLayout).sectionInset
}
override func layoutSubviews() {
super.layoutSubviews()
if self.gridLineContainer == nil {
self.gridLineContainer = CALayer()
self.gridLineContainer.frame = self.bounds
self.layer.addSublayer(self.gridLineContainer)
}
self.addNecessaryGridLayers()
self.updateVerticalLines()
self.updateHorizontalLines()
}
private func addNecessaryGridLayers() {
while self.verticalLines.count < cols - 1 {
let newLayer = CALayer()
newLayer.backgroundColor = UIColor.darkGray.cgColor
self.verticalLines.append(newLayer)
self.gridLineContainer.addSublayer(newLayer)
}
while self.horizontalLiines.count < rows + 1 {
let newLayer = CALayer()
newLayer.backgroundColor = UIColor.darkGray.cgColor
self.horizontalLiines.append(newLayer)
self.gridLineContainer.addSublayer(newLayer)
}
}
private func updateVerticalLines() {
for (idx,layer) in self.verticalLines.enumerated() {
let x = CGFloat(idx + 1) * self.colWidth
layer.frame = CGRect(x: x, y: 0, width: 1, height: CGFloat(self.rows) * self.rowHeight)
}
}
private func updateHorizontalLines() {
for (idx,layer) in self.horizontalLiines.enumerated() {
let y = CGFloat(idx) * self.rowHeight
layer.frame = CGRect(x: 0, y: y, width: CGFloat(self.cols) * self.colWidth, height: 1)
}
}
}
I solved this by subclassing the UICollectionViewFlowLayout and taking advantage of the layoutAttributesForElementsInRect method. With this you are able to set custom frame properties and allow overlapping. This is the most basic example:
#interface EqualBorderCellFlowLayout : UICollectionViewFlowLayout <UICollectionViewDelegateFlowLayout>
/*! #brief Border width. */
#property (strong, nonatomic) NSNumber *borderWidth;
#end
#import "EqualBorderCellFlowLayout.h"
#interface EqualBorderCellFlowLayout() { }
#end
#implementation EqualBorderCellFlowLayout
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if (self.borderWidth) {
CGFloat origin_x = attribute.frame.origin.x;
CGFloat origin_y = attribute.frame.origin.y;
CGFloat width = attribute.frame.size.width;
CGFloat height = attribute.frame.size.height;
if (attribute.frame.origin.x == 0) {
width = width + self.borderWidth.floatValue;
}
height = height + self.borderWidth.floatValue;
attribute.frame = CGRectMake(origin_x, origin_y, width, height);
}
}
return attributes;
}
- (CGFloat)minimumLineSpacing {
return 0;
}
- (CGFloat)minimumInteritemSpacing {
return 0;
}
- (UIEdgeInsets)sectionInset {
return UIEdgeInsetsMake(0, 0, 0, 0);
}
#end
The main challenge here is that its hard to come up with pixel perfect border/cell measurements due to screen WIDTH limitations. ie, there is almost always leftover pixels that cannot be divided. For example, if you want a 3-column grid with 1px inner borders, and the screen width is 375px, it means each column width has to be 124.333 px, which is not possible, so if you round down and use 124px as width, you ll have 1 px leftover. (Isnt it crazy how much that extra minuscule pixel affects the design?)
An option is to pick a border width that gives you perfect numbers, however thats not ideal cause you should not let chance dictate your design. Also, it potentially wont be a future proof solution.
I looked into the app that does this best, instagram, and as expected, their borders look pixel perfect. I checked out the width of each column, and it turns out, the last column is not the same width, which confirmed my suspicion. For the human eye its impossible to tell that a grid column is narrower or wider by a pixel, but its DEFINITELY visible when the borders are not the same width. So this approach seems to be the best
At first I tried using a single reusable cell view and update the width depending whether is the last column or not. This approach kinda worked, but the cells looked glitchy, because its not ideal to resize a cell. So I just created an additional reusable cell that would go at the last column to avoid resizing.
First, set the width and spacing of your collection and register 2 reusable identifiers for your collection view cell
let borderWidth : CGFloat = 1
let columnsCount : Int = 3
let cellWidth = (UIScreen.main.bounds.width - borderWidth*CGFloat(columnsCount-1))/CGFloat(columnsCount)
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: cellWidth, height: cellWidth)
layout.minimumLineSpacing = borderWidth
layout.minimumInteritemSpacing = borderWidth
collectionView = UICollectionView(frame: self.frame, collectionViewLayout: layout)
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: lastReuseIdentifier)
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
Then create a helper method to see if a cell at a certain index path is in the last column
func isLastColumn(indexPath: IndexPath) -> Bool {
return (indexPath.row % columnsCount) == columnsCount-1
}
Update the cellForItemAt delegate method for your collection view, so the last column cell is used
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let identifier = isLastColumn(indexPath: indexPath) ? lastReuseIdentifier : reuseIdentifier
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
return cell
}
And finally, update the size of the cells programatically, so the last column cell takes over all the leftover available space
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = isLastColumn(indexPath: indexPath) ? UIScreen.main.bounds.width-(cellWidth+borderWidth)*CGFloat(columnsCount -1) : cellWidth
return CGSize(width: width, height: cellWidth)
}
The end result should look something like this (with bonus food pics):