How to display a set number of rows in small Table View with self-sizing cells - swift

I have a custom table view class, which is configured to set the table view height to display only 3 rows. This means if there are 20 rows, table view will be sized to display first 3 rows, and allowing user to scroll.
This code of mine works only if I set the static rowHeight
class CannedRepliesTableView: UITableView {
/// The max visible rows visible in the autocomplete table before the user has to scroll throught them
let maxVisibleRows = 3
open override var intrinsicContentSize: CGSize {
let rows = numberOfRows(inSection: 0) < maxVisibleRows ? numberOfRows(inSection: 0) : maxVisibleRows
return CGSize(width: super.intrinsicContentSize.width, height: (CGFloat(rows) * rowHeight))
}
}
If I set UITableViewAutomaticDimension to the rowHeight, table view is not properly resized. Is there a solution to this?

This would be one way to improve on what you currently have. I don't have experience accessing intrinsicContentSize for this calculations and didn't test this locally (other than syntax), but if previously it worked, this should as well.
Basically you're creating an array with maxVisibleRows number of indexPaths. If you have less, then fetchedIndexesCount prevents an indexOutOfBounds crash. Once you have the array, you iterate for each corresponding cell and fetching its size, finally summing it up.
class CannedRepliesTableView: UITableView {
var focussedSection = 0
let maxVisibleRows = 3
open override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: calculateHeight())
}
private func calculateHeight() -> CGFloat {
guard let indexPaths = startIndexes(firstCount: 3) else {
// Your table view doesn't have any rows. Feel free to return a non optional and remove this check if confident there's always at least a row
return 0
}
return indexPaths.compactMap({ cellForRow(at: $0)?.intrinsicContentSize.height }).reduce(0, +)
}
private func startIndexes(firstCount x: Int) -> [IndexPath]? {
let rowsCount = numberOfRows(inSection: focussedSection)
let fetchedIndexesCount = min(x, rowsCount)
guard fetchedIndexesCount > 0 else {
return nil
}
var result = [IndexPath]()
for i in 0..<fetchedIndexesCount {
result.append(IndexPath(row: i, section: focussedSection))
}
return result
}
}

Related

Calendar-like UICollectionView - how to add left inset before first item only?

I have the following UICollectionView:
It has vertical scrolling, 1 section and 31 items.
It has the basic setup and I am calculating itemSize to fit exactly 7 per row.
Currently it looks like this:
However, I would like to make an inset before first item, so that the layout is even and there are the same number of items in first and last row. This is static and will always contain 31 items, so I am basically trying to add left space/inset before first item, so that it looks like this:
I have tried using a custom UICollectionViewDelegateFlowLayout method:
collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int)
But since there is only 1 section with 31 rows, it insets all of the rows, not just the first.
I know I could probably add two more "blank" items, but I feel like there is a better solution I may not be aware of. Any ideas?
EDIT: I've tried Tarun's answer, but this doesn't work. Origin of first item changes, but the rest stays as is, therefore first overlaps the second and the rest remain as they were. Shifting them all doesn't work either. I ended up with:
You need to subclass UICollectionViewFlowLayout and that will provide you a chance to customize the frame for all items within the collectionView.
import UIKit
class LeftAlignCellCollectionFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
guard let collectionView = self.collectionView else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
let leftMargin = self.sectionInset.left
let layout = collectionView.collectionViewLayout
for attribute in attributes {
if let cellAttribute = layout.layoutAttributesForItem(at: attribute.indexPath) {
// Check for `indexPath.item == 0` & do what you want
// cellAttribute.frame.origin.x = 0 // 80
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
}
Now you can use this custom layout class as your flow layout like following.
let flowLayout = LeftAlignCellCollectionFlowLayout()
collectionView.collectionViewLayout = flowLayout
Following Taran's suggestion, I've decided to use a custom UICollectionViewFlowLayout. Here is a generic answer that works for any number of items in the collectionView, as well as any inset value:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = self.collectionView else { return nil }
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
for attribute in attributes {
if let cellAttribute = collectionView.collectionViewLayout.layoutAttributesForItem(at: attribute.indexPath) {
let itemSize = cellAttribute.frame.size.width + self.minimumInteritemSpacing
let targetOriginX = cellAttribute.frame.origin.x + CGFloat(self.itemInset) * itemSize
if targetOriginX <= collectionView.bounds.size.width {
cellAttribute.frame.origin.x = targetOriginX
} else {
let shiftedPosition = lround(Double((targetOriginX / itemSize).truncatingRemainder(dividingBy: CGFloat(self.numberOfColumns))))
cellAttribute.frame.origin.x = itemSize * CGFloat(shiftedPosition)
cellAttribute.frame.origin.y += itemSize
}
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
where:
self.itemInset is the value we want to inset from the left (2 for my initial question, but it can be any number from 0 to the number of columns-1)
self.numberOfColumns is - as the name suggests - number of columns in the collectionView. This pertains to the number of days in my example and would always be equal to 7, but one might want this to be a generic value for some other use case.
Just for the sake of the completeness, I provide a method that calculates a size for my callendar collection view, based on the number of columns (days):
private func collectionViewItemSize() -> CGSize {
let dimension = self.collectionView.frame.size.width / CGFloat(Constants.numberOfDaysInWeek) - Constants.minimumInteritemSpacing
return CGSize(width: dimension, height: dimension)
}
For me, Constants.numberOfDaysInWeek is naturally 7, and Constants.minimumInteritemSpacing is equal to 2, but those can be any numbers you desire.

UICollectionViewFlowLayout with multiple columns and dynamic cell sizing

My goal is to implement a collection that looks like so:
Instagram's discover tab
I can achieve a similar result using UICollectionViewCompositionalLayout but I am constrained to iOS versions lower than 13.0, which makes this a no go.
I have been testing UICollectionViewFlowLayout and I can achieve multiple columns using collectionView(_:layout:sizeForItemAt:), but I can't find a way to dynamically calculate the height of the cells, which can be either an image, a label, etc.
Thanks
You need to subclass UICollectionViewFlowLayout which will provide you an opportunity to all the modifications to cell frames and more.
import UIKit
class InstagramCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// These attributes have all the information about how
// collectionView's default layout will layout your cells
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
guard let collectionView = self.collectionView else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
let layout = collectionView.collectionViewLayout
for attribute in attributes {
if let cellAttribute = layout.layoutAttributesForItem(at: attribute.indexPath) {
// Decide which indexPaths' cells should appear bigger
if attribute.indexPath.item % 2 == 0 {
// whatever modifications you want for big cells
cellAttribute.frame.size = CGSize(width: 200, height: 200)
}
else {
// whatever modifications you want for small cells
cellAttribute.frame.size = CGSize(width: 70, height: 70)
}
// Since you are modifying default layout and making some cells bigger
// You will need to accoun for extra height in your calculations
// I am not doing that here
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
}
Usage
let igFlowLayout = InstagramCollectionViewFlowLayout()
collectionView.collectionViewLayout = igFlowLayout
Notes
This is just an idea of how you are supposed to create a subclass to customize cell frames. You will need to add a lot more to this according to your own implementation.
Subclassing UICollectionViewLayout also means that now you are fully responsible for telling collectionView how big it's collectionViewContentSize should be for all your data.
Reference
Customizing Collection View Layouts has all the information you need to get started on this. They explain how to achieve a similar layout in their example.

Collectionview inside tableview causing issue with 2 columns

I have vertical collection-view inside tableview cell. collection view contain feature of load more too. for self sizing of collection view, i make tableview cell automaticDimension.
Also i have give height constant to collection-view. first time its loaded correctly but once i go to last cell and its load-more after reloading it create lot of space after collection view. can any one let me know what i am doing wrong here. or is there any other way around to make collection-view inside tableview self sizing so it increase tableview cell height too
**
TableviewCell Class
**
justForYouCollectionView.dataSource = self
justForYouCollectionView.delegate = self
justForYouCollectionView.isScrollEnabled = false
self.collectionHeight.constant = self.justForYouCollectionView.collectionViewLayout.collectionViewContentSize.height
justForYouCollectionView.reloadData()
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
self.layoutIfNeeded()
let contentSize = self.justForYouCollectionView.collectionViewLayout.collectionViewContentSize
return CGSize(width: contentSize.width, height: contentSize.height + 20)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.row == self.justForYouArray.count - 1 && self.isLoadMore {
updateNextSet()
}
}
**
CollectionViewCell Class
**
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
// Specify you want _full width_
let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
// Calculate the size (height) using Auto Layout
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
// Assign the new size to the layout attributes
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
extra space can be seen in image
I have worked on that earlier all I did is, set tableview height constraint set in storyboard and drop its outlet in viewController then after populate data get array count and divide by 2 and after dividing I multiply it by CollectionViewCell height and set that height to the UITableView height constraint like this.
let count = (array.count / 2) * cellHeight
tableviewHeightConstraint.constant = CGFloat(count)
This will solve your problem.
try
let a = (yourArray.count /2 ) * heightCell
tblViewHeightConstraint.constant = CGFloat(a)

Is there a way to get the number of cells in a table view?

In case if we are to take in all the cells in a TableView into an array and iterate through it to click on elements. I am looking for a solution in swift.
If each cell have a differentiating factor, then you get those in an array.
To use the text strings, I have to get hold of the cells first. So that leads to - how to get count count of cells and after getting hold of the cells, drilling down to see if this is the text, then open the contextual menu, else do something.
Here's what you can do: Keep using 'atIndex:' on the cells. Use selectElementWithMatcher::withError. Loop through until you find an indexOutOfBoundsError and then you should have the text.
But with the looping using atIndex:, you should have the cells that you want. And to do the same, see below:
for (int i = 0; i < someLargeValue; i++) {
EarlGrey.selectElementWithMatcher(grey_accessibilityID("abc")).atIndex(i)
}
-> Ok and for getting the value of “someLargeValue”, use selectElementWithMatcher::withError. Loop through until you find an indexOutOfBoundsError and then you should have the text.
This could be achieved by implementing a custom GREYAssertionBlock for the UITableView:
func assertTableView(_ accessibilityID: String, hasRowCount rowCount: Int, inSection section: Int) {
let cellCountAssert = GREYAssertionBlock(name: "cell count") { (element, error) -> Bool in
guard let tableView = element as? UITableView, tableView.numberOfSections > section else {
return false
}
let numberOfCells = tableView.numberOfRows(inSection: section)
return numberOfCells == rowCount
}
EarlGrey.selectElement(with: grey_accessibilityID(accessibilityID)).assert(cellCountAssert)
}

How to create grid of squares with no spaces between them?

I want a grid of squares with no spaces between them. Currently, I use custom UICollectionViewLayout:
class BoardViewLayout: UICollectionViewLayout {
override func collectionViewContentSize() -> CGSize {
return self.collectionView!.frame.size
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let boardSize = Int(sqrt(Double(self.collectionView!.numberOfItemsInSection(0))))
let index = indexPath.row
let size = self.collectionView!.frame.width / CGFloat(boardSize)
let xIndex = CGFloat(index%boardSize)
let yIndex = CGFloat(index/boardSize)
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.frame = CGRect(origin: CGPointMake(size*xIndex, size*yIndex), size: CGSizeMake(size, size))
return attributes
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesArray = [UICollectionViewLayoutAttributes]()
for i in 0..<self.collectionView!.numberOfItemsInSection(0) {
attributesArray.append(self.layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: i, inSection: 0))!)
}
return attributesArray
}
}
It looks like this:
screenshot of the result
I need to get rid of the spaces between third and fourth row and third and fourth column (see the screenshot).
Later, there will be square images instead of only colours (borders won't change) and there will be background that has to perfectly fit the images (so it looks like one image if all squares are filled and borders disappear)
I know it strongly depends on how system scales images etc., but there must be way to do this.
Thank you for help
You just need to set your minimum spacing to be 0:
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 0.0;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section{
return 0.0;
}