Different cell sizes with an collectionView based on constraints - ios

I've created a collectionView where i have 3 cells the first cells has to fill first row and the third should fill second row however i can't seem to figure out how to do this, since everytime i try to do in sizeForItemAt i get that the collectionView frame is (0, 0, 0, 0).
Here is what i have at the moment
class FlowLayout: UICollectionViewFlowLayout {
var numberOfCells: Int!
override init() {
super.init()
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLayout()
}
func setupLayout() {
minimumInteritemSpacing = 1
minimumLineSpacing = 1
}
override var itemSize: CGSize {
set {
}
get {
let numberOfColumns: CGFloat = 2
let itemWidth = (self.collectionView!.frame.width - (numberOfColumns - 1)) / numberOfColumns
let itemHeight = self.collectionView!.frame.height / 2
return CGSize(width: itemWidth, height: itemHeight)
}
}
}
illustration of what i want
setting up collectionView in my view
func createCollectionView() {
let layout = FlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.numberOfCells = 2
collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.backgroundColor = UIColor.white
collectionView.isScrollEnabled = false
self.topWrapperView.addSubview(collectionView)
collectionView.snp.makeConstraints { (make) -> Void in
make.left.equalTo(topWrapperView).offset(0)
make.top.equalTo(topWrapperView).offset(0)
make.bottom.equalTo(topWrapperView).offset(0)
make.right.equalTo(topWrapperView).offset(0)
}
}

I had a similar situation except it was the first cell that I wanted 2 cells in first row, with 3 each row after. Used it for a main profile pic and bio in a collection view of images. I successfully implemented it using sizeForItemAtIndexPath with default UICollectionViewDelegateFlowLayout.
Here is something that should work for your situation:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let insets = collectionView.contentInset
let collectionViewWidth = collectionView.frame.width - (insets.left + insets.right + 1)
let collectionViewHeight = collectionView.frame.height - (insets.top + insets.bottom)
switch indexPath.row {
case 0, 1:
return CGSize(width: collectionViewWidth / 2, height: collectionViewHeight / 2)
default:
return CGSize(width: collectionViewWidth, height: collectionViewHeight / 2)
}
}
You can change what you divide height by to get desired height.
This is what I wrote for my situation, figured I'd post it incase it can help you or anyone else who reads it. If you want it to change with rotation, use viewWillTransition and set a bool flag with a didSet listener:
var isPortraitOrientation: Bool = true {
didSet {
if oldValue != isPortraitOrientation {
collectionView?.collectionViewLayout.invalidateLayout()
}
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
isPortraitOrientation = (size.width - size.height) <= 0 ? true : false
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let insets = collectionView.contentInset
let collectionViewWidth = collectionView.frame.width - (insets.left + insets.right + 1)
let collectionViewHeight = collectionView.frame.height - (insets.top + insets.bottom)
switch indexPath.row {
case 0:
if textViewSelected {
return isPortraitOrientation ? CGSize(width: collectionViewWidth, height: collectionViewHeight / 2) : CGSize(width: collectionViewWidth, height: collectionViewWidth / 3)
} else {
return isPortraitOrientation ? CGSize(width: collectionViewWidth / 2, height: collectionViewWidth / 2) : CGSize(width: collectionViewWidth / 2, height: collectionViewHeight / 1.5)
}
case 1:
return isPortraitOrientation ? CGSize(width: collectionViewWidth / 2, height: collectionViewWidth / 2) : CGSize(width: collectionViewWidth / 2, height: collectionViewHeight / 1.5)
default:
return isPortraitOrientation ? CGSize(width: collectionViewWidth / 3, height: collectionViewWidth / 3) : CGSize(width: collectionViewWidth / 5, height: collectionViewWidth / 5)
}
}
EDIT:
based on your additional code, instead of doing CGRect.zero for the collectionView frame use topWrapperView.bounds.

Related

How to draw grid lines for UICollectionView cells that are independent of device size?

Currently I am using UICollectionView to make a grid of items. I want to draw grid lines as in the screenshot that works on both iPhones and iPads.
On iPad, either the label or the grid line is not correctly placed. On iPhone, the adjustments worked fine.
override func viewDidLoad() {
self.automaticallyAdjustsScrollViewInsets = false
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 1.0
layout.minimumInteritemSpacing = 0.0
collectionView.setCollectionViewLayout(layout, animated: false)
collectionView.layoutIfNeeded()
collectionView.contentOffset = CGPoint.zero
}
private let foundationScreenElems = ["ABOUT US", "CHAIRMAN'S MESSAGE", "MESSAGE OF CEO", "OUR GOALS", "PILLARS", "VISION AND MISSION", "QUALITY POLICY", "TERMS & CONDITIONS"]
var foundationGridLayers: [CALayer] = []
func drawHorizontalLines(rowsCount: Int) {
var pos = UIDevice.current.userInterfaceIdiom == .pad ? collectionView.frame.width / 3.5 - 0.5 : collectionView.frame.width / 2.5 - 0.5
var i = 1
while i < rowsCount {
if (viewId == .foundation && foundationGridLayers.count < 4) {
let layer = CALayer()
if (i == 3 && UIDevice.current.userInterfaceIdiom == .pad) {
pos = pos + 10
}
layer.frame = CGRect(x: 10, y: CGFloat(i) * pos, width: self.view.frame.width - 20, height: 1.0)
layer.backgroundColor = gridColor
foundationGridLayers.append(layer)
collectionView.layer.addSublayer(layer)
}
i += 1
}
}
func drawVerticalLine(rowsCount: Int) {
let top = getTop()
let pos = UIDevice.current.userInterfaceIdiom == .pad ? collectionView.frame.width / 3.5 - 0.5 : collectionView.frame.width / 2.5 - 0.5
let width = self.view.frame.width / 2
let layer = CALayer()
if (UIDevice.current.userInterfaceIdiom == .pad) {
layer.frame = CGRect(x: width, y: top + 10, width: 1.0, height: pos * CGFloat(rowsCount) + 30)
} else {
layer.frame = CGRect(x: width, y: top - 10, width: 1.0, height: pos * CGFloat(rowsCount) - 10)
}
layer.backgroundColor = gridColor
collectionView.layer.addSublayer(layer)
}
func getTop() -> CGFloat {
let pos = UIDevice.current.userInterfaceIdiom == .pad ? collectionView.frame.width / 3 - 0.5 : collectionView.frame.width / 2.5 - 0.5
let height = collectionView.frame.height
let rowCount: CGFloat = CGFloat(foundationScreenElems.count / 2)
let emptySpace = height - (pos * rowCount)
return UIDevice.current.userInterfaceIdiom == .pad ? emptySpace / 2 : emptySpace / 4
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let top = getTop()
return UIEdgeInsets(top: top, left: 0, bottom: 10, right: 0)
}
extension GridViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width / 2 - 0.5
let height = UIDevice.current.userInterfaceIdiom == .pad ? collectionView.frame.width / 3 - 0.5 : collectionView.frame.width / 2.5 - 0.5
return CGSize(width: width, height: height)
}
}
On iPad, the Pillars rows is not aligned properly or the grid line is not at right position, but it works fine on iPhone.
Is there a way to draw grid lines independent of devices?
You can try creating 2 different types of custom cell, One for left and one for right with labels filled with border color to look.
You can have a look at this sample
In the collection view controller you can check for the cell and create the appropriate cell.
override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
var reuseIdentifier :String = "leftCell"
let itemsPerRow = 2;
let column = indexPath.item % itemsPerRow;
if(column == 1)
{
reuseIdentifier = "rightCell"
}
let cell = collectionView
.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
// Configure the cell
return cell
}
Make the cell fit any device screen
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/3);
}
Add appropriate constraints so that the border labels always maintain a zero margin to the right

UICollection view cell with large text problem

I have a cell with description(UILabel) and two buttons after the description I use this to get the description text height
let constraintRect = CGSize(width: maxWidth, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedStringKey.font: font], context: nil)
return ceil(boundingBox.height)
but the problem is when I do any action on the buttons with a huge description cell the actions is not trigged this is a ver annoying a problem and tried a lot of solutions but nothing works any help ?
this is where I set the height for cells
override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
if self.items.count < 1 {
return CGSize(width: (self.view.bounds.width - 16), height: 200)
}
if indexPath.item < self.items.count {
let item = self.items[indexPath.item]
var h = CGFloat(0)
h = item["description"].stringValue.height(with: (self.collectionView.bounds.width - 44), font: UIFont(name: "Helvetica", size: self.contentLabelFontSize)!)
self.descriptionHeight = h
if item["images"].isEmpty == false {
if item["images"][0]["original"].stringValue != "" {
let hi = 300
let im_h = CGFloat(hi)
h += im_h
}
}
if self.type == "events" {
h += 260
}
else if item["type"].stringValue == "event" {
h += 250
}
else {
h += 170
}
let size = CGSize(width: (self.view.bounds.width - 16), height: h)
return size
}
return CGSize(width: (self.view.bounds.width - 16), height: 50)
}
And this is where is set the height of label at the PostCell class :
self.contentLabel.frame = CGRect(x: 10, y: 110, width: (self.bounds.width - 20), height: (self.bounds.height - self.imageHeight - 170))

Rotation Lanscape collectionView

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

How to set different height and width of cells in horizontal collectionview

I have a horizontal collection view and it shows 5 cells at a time and i have adjusted that through sizeForItemAt :
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (self.numberCollectionView?.bounds.size.width)!/5 - 3, height: (self.numberCollectionView?.bounds.size.width)!/5 - 3 )
}
But these 5 cells would have different width and height and there is a ratio to it. So the center item would take 40% of the whole size, the 2nd and 3rd item would take 20% of the size and the 1st and 5th would take 10% of the size. According to this ratio items size would get change on scroll. These items are all Label.
Edit:
unlike the suggested question and other questions in stackoverflow in my case there is only one row and i have custom UICollectionViewFlowLayout to handle the scrolling by cell
UICollectionViewFlowLayout to handle the scrolling by cell:
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
Then to find the center cell of the CollectionView:
private func findCenterIndex() -> Int {
let center = self.view.convert(numberCollectionView.center, to: self.numberCollectionView)
let row = numberCollectionView!.indexPathForItem(at: center)?.row
guard let index = row else {
return 0
}
self.rating = index
return index
}
under scrollViewDidScrolltransformed cell:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
transformCell()
}
private func transformCell() {
let row = findCenterIndex()
var offset : Float = 0.0
for cell in numberCollectionView.visibleCells as! [VotingCell] {
if row == numberCollectionView.indexPath(for: cell)?.row {
offset = 1.1
cell.label.font = UIFont.boldSystemFont(ofSize: 28.0)
cell.label.textColor = UIColor.black
cell.alpha = 1.0
cell.label.textAlignment = .center
}
else if row == (numberCollectionView.indexPath(for: cell)?.row)! + 1 || row == (numberCollectionView.indexPath(for: cell)?.row)! - 1
{
offset = 0.8
cell.label.font = UIFont(name: cell.label.font.fontName, size: 26.0)
cell.label.textColor = UIColor.gray
cell.alpha = 0.8
cell.label.textAlignment = .center
}
else if row == (numberCollectionView.indexPath(for: cell)?.row)! + 2 || row == (numberCollectionView.indexPath(for: cell)?.row)! - 2 {
offset = 0.7
cell.label.font = UIFont(name: cell.label.font.fontName, size: 20.0)
cell.label.textColor = UIColor.gray
cell.alpha = 0.8
cell.label.textAlignment = .center
}
else
{
offset = 0.00
}
cell.transform = CGAffineTransform.identity
cell.transform = CGAffineTransform(scaleX: CGFloat(offset), y: CGFloat(offset))
}
}
also called transformCell() from viewDidLayoutSubviews for initial load.

Handle scrollview size in UICollectionViewCell

Im making an horizontal UICollectionView, and inside UICollectionViewCell i have scrollview and inside the scrollview i have an imageView.
The issue I'm having is that when i zoom-in the imageView,scrollView is taking all the cell size, so its not fitting to the image size height and width.thus by scrolling up and down the image disappear from scrollview, i have no idea whats going wrong in my code.
My ColectionViewCell code:
class CollectionViewCell: UICollectionViewCell {
#IBOutlet var scrollView: UIScrollView!
#IBOutlet var ImageV: UIImageView!
}
CollectionView code :
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! CollectionViewCell
cell.scrollView.contentMode = UIViewContentMode.ScaleAspectFit
cell.scrollView.delegate = self
cell.ImageV.image = UIImage(named: array[indexPath.row])
cell.ImageV.contentMode = UIViewContentMode.ScaleAspectFit
cell.scrollView.minimumZoomScale = 1
cell.scrollView.maximumZoomScale = 4;
cell.scrollView.contentSize = cell.ImageV.frame.size
return cell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: self.collectionView.frame.size.width , height: self.collectionView.frame.size.height - 100)
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
let indexPath = NSIndexPath(forItem: Int(currentIndex), inSection: 0)
if let cell1 = self.collectionView.cellForItemAtIndexPath(indexPath) {
let cell = cell1 as! CollectionViewCell
let boundsSize = cell.scrollView.bounds.size
var contentsFrame = cell.ImageV.frame
if contentsFrame.size.width < boundsSize.width{
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2
}else{
contentsFrame.origin.x = 0
}
if contentsFrame.size.height < boundsSize.height {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2
}else{
contentsFrame.origin.y = 0
}
return cell.ImageV
}
return UIView()
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width;
oldcell = currentIndex - 1
let indexPath = NSIndexPath(forItem: Int(oldcell), inSection: 0)
if let cell1 = self.collectionView.cellForItemAtIndexPath(indexPath) {
let cell = cell1 as! CollectionViewCell
cell.scrollView.zoomScale = 0
}
}
Image preview:
https://i.imgur.com/Gr2p09A.gifv
My project found here : https://drive.google.com/file/d/0B32ROW7V8Fj4RVZfVGliXzJseGM/view?usp=sharing
Well First lets start with UIViewController that is holding your UICollectionView:
Define a variable to hold the collection view layout:
var flowLayout:UICollectionViewFlowLayout = UICollectionViewFlowLayout()
You will have to override viewWillLayoutSubviews and this is going to handle the collectionView size.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
flowLayout.itemSize = CGSize(width: view.frame.width, height:view.frame.height)
}
Also you will need to override viewDidLayoutSubviews to handle the size of each new cell to set it to default size:
override func viewDidLayoutSubviews() {
if let currentCell = imageCollectionView.cellForItemAtIndexPath(desiredIndexPath) as? GalleryCell {
currentCell.configureForNewImage()
}
}
in ViewDidLoad setup the collectionView to be horizontal with flow layout:
// Set up flow layout
flowLayout.scrollDirection = UICollectionViewScrollDirection.Horizontal
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
// Set up collection view
imageCollectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height), collectionViewLayout: flowLayout)
imageCollectionView.translatesAutoresizingMaskIntoConstraints = false
imageCollectionView.registerClass(GalleryCell.self, forCellWithReuseIdentifier: "GalleryCell")
imageCollectionView.dataSource = self
imageCollectionView.delegate = self
imageCollectionView.pagingEnabled = true
view.addSubview(imageCollectionView)
imageCollectionView.contentSize = CGSize(width: 1000.0, height: 1.0)
In your UICollectionView method cellForItemAtIndexPath load the image only without setting anything:
cell.image = UIImage(named: array[indexPath.row])
Now lets move to GalleryCell class and to handle scrollView when zooming:
class GalleryCell: UICollectionViewCell, UIScrollViewDelegate {
var image:UIImage? {
didSet {
configureForNewImage()
}
}
var scrollView: UIScrollView
let imageView: UIImageView
override init(frame: CGRect) {
imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
scrollView = UIScrollView(frame: frame)
scrollView.translatesAutoresizingMaskIntoConstraints = false
super.init(frame: frame)
contentView.addSubview(scrollView)
contentView.addConstraints(scrollViewConstraints)
scrollView.addSubview(imageView)
scrollView.delegate = self
scrollView.maximumZoomScale = 4
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func configureForNewImage() {
imageView.image = image
imageView.sizeToFit()
setZoomScale()
scrollViewDidZoom(scrollView)
}
public func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
public func scrollViewDidZoom(scrollView: UIScrollView) {
let imageViewSize = imageView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
if verticalPadding >= 0 {
// Center the image on screen
scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
} else {
// Limit the image panning to the screen bounds
scrollView.contentSize = imageViewSize
}
}
I've tested it with example and it should solve your issue with scrollview and #Spencevail explained mostly why it's being caused !
You need to set the contentInset on your scrollview. What's happening is The contentSize of the UIScrollView is originally set to match the screen size with the image inside of that. As you zoom in on the scrollview, the contentSize expands proportionately, so those black areas above and below the photos when you're zoomed all the way out expand as you zoom in. In other words You're expanding the area above and below where your image can go. If you adjust the contentInset it will bring in that dead area and not allow the scrollview to move the image out of the window.
public func scrollViewDidZoom(scrollView: UIScrollView) {
let imageViewSize = imageView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
if verticalPadding >= 0 {
// Center the image on screen
scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
} else {
// Limit the image panning to the screen bounds
scrollView.contentSize = imageViewSize
}
}
This looks almost the same as an open source Cocoapod I help manage, SwiftPhotoGallery. Take a look at the code for the SwiftPhotoGalleryCell and you should get a pretty good idea of how to do this. (Feel free to just use the cocoapod too if you want!)

Resources