UITableview with dynamic height cells & filling footer view - swift

I am trying to make a UITableview with a footer view that fills the remaining empty space of the UITableview. However, the catch is that the cells have varying heights.
I am aware that, if the cells were all a static height, I could simply count how many cells there are and multiply that by the static height. Unfortunately, that won't work in this case.
Current solution:
Works for static height cells.
Does not work for dynamic height cells.
private func adjustFooterHeight() {
if data.count == 0 {
footerView.frame.size.height = tableView.bounds.height
}
else {
//I use data.count instead of (data.count - 1) as I have the extra hidden cell, as pictured in image 2.
let bottomFrame = tableView.rectForRow(at: IndexPath(row: data.count, section: 0))
let height = tableView.bounds.height - (bottomFrame.origin.y + bottomFrame.height - rowHeight)
footerView.frame.size.height = height < 0 ? 0 : height
tableView.reloadData()
}
}
Diagram of Intentions:

So I figured it out...
Here is the solution.
private func adjustFooterHeight() {
//Get content height & calculate new footer height.
let cells = self.tableView.visibleCells
var height: CGFloat = 0
for i in 0..<cells.count {
height += cells[i].frame.height
}
height = self.tableView.bounds.height - ceil(height)
//If the footer's new height is negative, we make it 0, since we don't need footer anymore.
height = height > 0 ? height : 0
//Create the footer
let footerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: height))
self.tableView.tableFooterView = footerView
}

Related

Fixing wrong constraints when UITableViewCell is first loaded

I have a tableview which I am loading images in, and setting constraints on the imageview in cellForRowAt via the following function:
func setImageConstraints(height: Double, width: Double) {
if (width > height) {
let ratio = self.photoView.frame.width / width
let newHeight = height * ratio
let newWidth = self.maxWidth
self.photoWidthConstraint.constant = newWidth
self.photoHeightConstraint.constant = newHeight
self.photoWidthConstraint.isActive = true
self.photoHeightConstraint.isActive = true
}
else {
let ratio = self.photoView.frame.height / height
let newHeight = self.maxHeight
let newWidth = width * ratio
self.photoWidthConstraint.constant = newWidth
self.photoHeightConstraint.constant = newHeight
self.photoWidthConstraint.isActive = true
self.photoHeightConstraint.isActive = true
}
}
The problem is, when the cell is first loaded the images are not the right heights/width, until the cell is scrolled out of view, and then back into view. I can't seem to figure out how to get this working when the cell is initially loaded though.
Set the constraints for the cell in the tableView:willDisplayCell:forRowAtIndexPath: method.
In the cellForRowAtIndexPath: method, the cell's frame isn't setup yet and the cell isn't yet added to the table view.

Buttons on UIScrollView bounces back when centered

My UIScrollView bounces back from left side, but stays normal from the right side.
Look at the gif please.
Important note: The buttons should be centered when the screen opens.
To center my buttons, I create them like this:
button.frame = CGRect(
x: screenWidth/2 + gap,
y: 0,
width: buttonWidth,
height: buttonHeight)
buttonsScrollView.addSubview(button)
I already set wide content size
buttonsScrollView.contentSize.width = screenWidth
and adding extra space - screenWidth + 100 does not help, it makes wider only from the right side.
You can do this with "calculated" frames or with auto-layout. In either case, the general idea...
Add your buttons to a UIView - we'll call it buttonsView:
Start the first button at gap distance from Zero, and set the frame of buttonsView to the height of the buttons, and add gap distance to the right-end.
Add buttonsView to the scroll view, at x: 0, y: 0.
Set the scroll view's .contentSize = buttonsView.frame.size.
Do all of that in viewDidLoad().
We can't center the buttonsView until we know the scroll view's frame width, which will likely vary depending on device (and if you rotate the device), so...
Add a class property to track the scroll view's width:
// track the scroll view frame width
var scrollViewWidth: CGFloat = 0
then, in viewDidLayoutSubviews() we know the scroll view's frame, so we'll adjust the .contentOffset.x to horizontally center the buttonsView:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// here we know frame size, but
// viewDidLayoutSubviews() can (and usually will) be called multiple times
// so we only want to execute this code when the scroll view width changes
if scrollViewWidth != scrollView.frame.width {
scrollViewWidth = scrollView.frame.width
// calculate content offset x so the row of buttons is centered horizontally
scrollView.contentOffset.x = (buttonsView.frame.width - scrollView.frame.width) * 0.5
}
}
Here's a complete sample implementation. No #IBOutlet connections -- everything is done via code -- so just set a view controller's class to CenterScrollViewController:
class CenterScrollViewController: UIViewController {
// create a scroll view
let scrollView: UIScrollView = {
let v = UIScrollView()
// background color so we can see its frame
v.backgroundColor = .systemYellow
return v
}()
// create a buttons holder view
let buttonsView: UIView = {
let v = UIView()
return v
}()
// gap between buttons and on left/right sides of button row
let gap: CGFloat = 12
// buttons will be (round) at 44 x 44 points
let btnSize: CGFloat = 44
// number of buttons
let numButtons: Int = 9
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
title = "Calc"
// add scroll view to view
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
// add buttons view to scroll view
scrollView.addSubview(buttonsView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view
// Top + 40
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
// Leading and Trailing
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// Height equal to button height + "padding" on bottom
scrollView.heightAnchor.constraint(equalToConstant: btnSize + gap),
])
// let's add some buttons to the buttons view
var x: CGFloat = gap
for i in 1...numButtons {
let b = UIButton()
b.backgroundColor = .systemBlue
b.setTitle("\(i)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
// let's keep the buttons square (1:1 ratio) so we can make them round
b.frame = CGRect(x: x, y: 0, width: btnSize, height: btnSize)
b.layer.cornerRadius = btnSize * 0.5
b.layer.borderColor = UIColor.black.cgColor
b.layer.borderWidth = 1
buttonsView.addSubview(b)
x += btnSize + gap
}
// x now equals the total buttons width plus gap on each side
// so set the frame size of the buttons view to (x, btnSize)
buttonsView.frame = CGRect(x: 0, y: 0, width: x, height: btnSize)
// set scroll view content size to the size of the buttons view
scrollView.contentSize = buttonsView.frame.size
}
// track the scroll view frame width
var scrollViewWidth: CGFloat = 0
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// here we know frame size, but
// viewDidLayoutSubviews() can (and usually will) be called multiple times
// so we only want to execute this code when the scroll view width changes
if scrollViewWidth != scrollView.frame.width {
scrollViewWidth = scrollView.frame.width
// calculate content offset x so the row of buttons is centered horizontally
scrollView.contentOffset.x = (buttonsView.frame.width - scrollView.frame.width) * 0.5
}
}
}

How to set Size for a specific cell when using dynamic size uicollectionView cell

I'm following this tutorial:
https://www.raywenderlich.com/4829472-uicollectionview-custom-layout-tutorial-pinterest
To achieve this layout
Basically a horizontal collectionView in section 1 and a vertical collectionView in section 2
My thought was putting a collectionView inside the first cell, which should have the size: CGSize(width: screen.width, height: 100)
But since I'm using the method from the link, I don't know how to give a specific size for the first cell. This is what I've tried:
Disclaimer: As OP had asked a question earlier on how to create a Pinterest layout and I had suggested Raywenderlich tutorial in comments, I believe this question from OP is extension of that
Answer:
Looking at the design you added, I believe you are trying to add a CollectionView with a horizontal scroll as the first cell which spans the entire collection view width. Because you have subclassed the UICollectionViewFlowLayout you need to handle this special case in prepare method itself
override func prepare() {
// 1
guard
cache.isEmpty,
let collectionView = collectionView
else {
return
}
// 2
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset: [CGFloat] = []
for column in 0..<numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset: [CGFloat] = .init(repeating: 0, count: numberOfColumns)
// 3
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let attributes: UICollectionViewLayoutAttributes
let photoHeight = delegate?.collectionView(
collectionView,
heightForPhotoAtIndexPath: indexPath) ?? 180
let height = cellPadding * 2 + photoHeight
let frame: CGRect
if item == 0 {
frame = CGRect(x: 0,
y: yOffset[column],
width: columnWidth,
height: height)
contentHeight = frame.maxY
for column in 0..<numberOfColumns {
yOffset[column] = frame.maxY
}
column = 0
}
else {
// 4
frame = CGRect(x: xOffset[column],
y: yOffset[column],
width: columnWidth,
height: height)
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
}
}
In your for item in 0..<collectionView.numberOfItems(inSection: 0) { I have added a special condition for handling if item == 0 { where in instead of using column width, I have used content width (as per your design you want first cell to cover the whole width of collection view)
Finally in your delegate method
extension PhotoStreamViewController: PinterestLayoutDelegate {
func collectionView(
_ collectionView: UICollectionView,
heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
if indexPath.row == 0 {
return 180 // whatever is the height of first cell
}
return photos[indexPath.item].image.size.height
}
}
A word of caution
In your design I can see that you have added two section headers Header 1 and Header 2 I guess in that case your returning number of sections as at least 2, but the tutorial focuses on a single section collection view, if you notice
for item in 0..<collectionView.numberOfItems(inSection: 0) {
section 0 is hard coded, you might have to handle it dynamically if you wanna handle multiple sections and might need a lot of reworking around prepare
A strategy you might follow to gain the ability to control the size of your collection items is to have your delegate for the UICollection view derive from UICollectionViewDelegateFlowLayout instead of just from UICollectionViewDelegate. By deriving from UICollectionViewDelegateFlowLayout your delegate can implement:
func collectionView(UICollectionView, layout: UICollectionViewLayout, sizeForItemAt: IndexPath) -> CGSize
And tell the system the size of the first item.

Adding UIButtons dynamically in UICollectionViewCell - Swift

Trying to show buttons dynamically in a UICollectionViewCell depends on data coming in array from api. Below is what trying to achieve:
This is so far what i have achieved using button, but i need multiple buttons means button next to "About us" then next until space there then move to next line and setting height of cell as per height of view in cell. Please guide how it can be achieved.
Below is my code:
var xPosition = cell.btnOptions.frame.origin.x
var yPosition = cell.btnOptions.frame.origin.y
var width = cell.btnOptions.frame.size.width
var height = cell.btnOptions.frame.size.height
if((arrOptions?.count ?? 0) > 0)
{
for i in 0..<arrOptions!.count
{
//
cell.btnOptions.tag = i
cell.btnOptions.layer.cornerRadius = 3.0
cell.btnOptions.frame = CGRect.init(x: xPosition, y: yPosition, width: width, height: height)
//
cell.btnOptions.setTitle(arrOptions?.objectAt(i).object(forKey: "title") as? String, for: .normal)
xPosition = width + width + 8
yPosition = height
}

Issue with custom UICollectionViewLayout leaving gap at bottom of cell

I have a UICollectionView with a custom layout. As seen in the image below there is always a gap between the content and the bottom of the cell.
Each cell consists of an image and three labels. I've printed out the size of the of the cell and also printed out the size of the image + the 3 lables and the match, so I have no idea where this extra space is coming from.
let width = columnWidth - cellPadding*2
let photoHeight = delegate.collectionView(collectionView!, heightForPhotoAtIndexPath: indexPath , withWidth:width)
print("photo height is: \(photoHeight)")//photo height is: 95.4078125
let label1Height = delegate.collectionView(collectionView!, heightForLabel1AtIndexPath: indexPath, withWidth: width)
print("label1Height is \(label1Height)") // label1Height is 39.0
let label2Height = delegate.collectionView(collectionView!,heightForLabel2AtIndexPath: indexPath, withWidth: width) //label2Height is 53.0
print("label2Height is \(label2Height)")// label2Height is 53.0
let label3Height = delegate.collectionView(collectionView!, heightForLabel3AtIndexPath: indexPath, withWidth: width)
print("label3Height is \(label3Height)") //label3Height is 135.0
let height = photoHeight + label1Height + label2Height + label3Height
print("total height is: \(height)") //total height is: 322.4078125
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
I've pinned the bottom of label3 to the bottom of the cell +4, but that doesn't help