I'm using a UICollectionView with a flow layout to show a list of cells, I also have a page control to indicate current page, but there seems to be no way to get current index path, I know I can get visible cells:
UICollectionView current visible cell index
however there can be more than one visible cells, even if each of my cells occupies full width of the screen, if I scroll it to have two halves of two cells, then they are both visible, so is there a way to get only one current visible cell's index?
Thanks
You can get the current index by monitoring contentOffset in scrollViewDidScroll delegate
it will be something like this
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSInteger currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width;
}
Get page via NSIndexPath from center of view.
Works even your page not equal to width of UICollectionView.
func scrollViewDidScroll(scrollView: UIScrollView) {
let xPoint = scrollView.contentOffset.x + scrollView.frame.width / 2
let yPoint = scrollView.frame.height / 2
let center = CGPoint(x: xPoint, y: yPoint)
if let ip = collectionView.indexPathForItemAtPoint(center) {
self.pageControl.currentPage = ip.row
}
}
Definitely you need catch the visible item when the scroll movement is stopped. Use next code to do it.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let indexPath = myCollectionView.indexPathsForVisibleItems.first {
myPageControl.currentPage = indexPath.row
}
}
Swift 5.1
The easy way and more safety from nil crash
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if collectionView == newsCollectionView {
if newsPager.currentPage == indexPath.row {
guard let visible = newsCollectionView.visibleCells.first else { return }
guard let index = newsCollectionView.indexPath(for: visible)?.row else { return }
newsPager.currentPage = index
}
}
}
Place PageControl in your view or set by Code.
Set UIScrollViewDelegate
In Collectionview-> cellForItemAtIndexPath (Method) add the below
code for calculate the Number of pages,
int pages = floor(ImageCollectionView.contentSize.width/ImageCollectionView.frame.size.width);
[pageControl setNumberOfPages:pages];
Add the ScrollView Delegate method,
#pragma mark - UIScrollViewDelegate for UIPageControl
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat pageWidth = ImageCollectionView.frame.size.width;
float currentPage = ImageCollectionView.contentOffset.x / pageWidth;
if (0.0f != fmodf(currentPage, 1.0f))
{
pageControl.currentPage = currentPage + 1;
}
else
{
pageControl.currentPage = currentPage;
}
NSLog(#"finishPage: %ld", (long)pageControl.currentPage);
}
I had similar situation where my flow layout was set for UICollectionViewScrollDirectionHorizontal and I was using page control to show the current page.
I achieved it using custom flow layout.
/------------------------
Header file (.h) for custom header
------------------------/
/**
* The customViewFlowLayoutDelegate protocol defines methods that let you coordinate with
*location of cell which is centered.
*/
#protocol CustomViewFlowLayoutDelegate <UICollectionViewDelegateFlowLayout>
/** Informs delegate about location of centered cell in grid.
* Delegate should use this location 'indexPath' information to
* adjust it's conten associated with this cell.
* #param indexpath of cell in collection view which is centered.
*/
- (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout cellCenteredAtIndexPath:(NSIndexPath *)indexPath;
#end
#interface customViewFlowLayout : UICollectionViewFlowLayout
#property (nonatomic, weak) id<CustomViewFlowLayoutDelegate> delegate;
#end
/------------------- Implementation file (.m) for custom header -------------------/
#implementation customViewFlowLayout
- (void)prepareLayout {
[super prepareLayout];
}
static const CGFloat ACTIVE_DISTANCE = 10.0f; //Distance of given cell from center of visible rect
static const CGFloat ITEM_SIZE = 40.0f; // Width/Height of cell.
- (id)init {
if (self = [super init]) {
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.minimumInteritemSpacing = 60.0f;
self.sectionInset = UIEdgeInsetsZero;
self.itemSize = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
self.minimumLineSpacing = 0;
}
return self;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if (CGRectIntersectsRect(attribute.frame, rect)) {
CGFloat distance = CGRectGetMidX(visibleRect) - attribute.center.x;
// Make sure given cell is center
if (ABS(distance) < ACTIVE_DISTANCE) {
[self.delegate collectionView:self.collectionView layout:self cellCenteredAtIndexPath:attribute.indexPath];
}
}
}
return attributes;
}
Your class containing collection view must conform to protocol 'CustomViewFlowLayoutDelegate' I described earlier in custom layout header file. Like:
#interface MyCollectionViewController () <UICollectionViewDataSource, UICollectionViewDelegate, CustomViewFlowLayoutDelegate>
#property (strong, nonatomic) IBOutlet UICollectionView *collectionView;
#property (strong, nonatomic) IBOutlet UIPageControl *pageControl;
....
....
#end
There are two ways to hook your custom layout to collection view, either in xib OR in code like say in viewDidLoad:
customViewFlowLayout *flowLayout = [[customViewFlowLayout alloc]init];
flowLayout.delegate = self;
self.collectionView.collectionViewLayout = flowLayout;
self.collectionView.pagingEnabled = YES; //Matching your situation probably?
Last thing, in MyCollectionViewController implementation file, implement delegate method of 'CustomViewFlowLayoutDelegate'.
- (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout cellCenteredAtIndexPath:(NSIndexPath *)indexPath {
self.pageControl.currentPage = indexPath.row;
}
I hope this would be helpful. :)
Note - I have found andykkt's answer useful but since it is in obj-c converted it to swift and also implemented logic in another UIScrollView delegate for a smoother effect.
func updatePageNumber() {
// If not case to `Int` will give an error.
let currentPage = Int(ceil(scrollView.contentOffset.x / scrollView.frame.size.width))
pageControl.currentPage = currentPage
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// This will be call when you scrolls it manually.
updatePageNumber()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
// This will be call when you scrolls it programmatically.
updatePageNumber()
}
for swift 4.2
#IBOutlet weak var mPageControl: UIPageControl!
#IBOutlet weak var mCollectionSlider: UICollectionView!
private var _currentIndex = 0
private var T1:Timer!
private var _indexPath:IndexPath = [0,0]
private func _GenerateNextPage(){
self._currentIndex = mCollectionSlider.indexPathForItem(at: CGPoint.init(x: CGRect.init(origin: mCollectionSlider.contentOffset, size: mCollectionSlider.bounds.size).midX, y: CGRect.init(origin: mCollectionSlider.contentOffset, size: mCollectionSlider.bounds.size).midY))?.item ?? 0
self.mPageControl.currentPage = self._currentIndex
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
_SetTimer(AutoScrollInterval)
_GenerateNextPage()
}
#objc private func _AutoScroll(){
self._indexPath = IndexPath.init(item: self._currentIndex+1, section: 0)
if !(self._indexPath.item < self.numberOfItems){
_indexPath = [0,0]
}
self.mCollectionSlider.scrollToItem(at: self._indexPath, at: .centeredHorizontally, animated: true)
}
private func _SetTimer(_ interval:TimeInterval){
if T1 == nil{
T1 = Timer.scheduledTimer(timeInterval: interval , target:self , selector: #selector(_AutoScroll), userInfo: nil, repeats: true)
}
}
you can skip the function _SetTimer() , thats for auto scroll
With UICollectionViewDelegate methods
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
pageControl.currentPage = indexPath.row
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if pageControl.currentPage == indexPath.row {
pageControl.currentPage = collectionView.indexPath(for: collectionView.visibleCells.first!)!.row
}
}
Swift 5.0
extension youriewControllerName:UIScrollViewDelegate{
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = self.collectionView.frame.size.width
pageControl.currentPage = Int(self.collectionView.contentOffset.x / pageWidth)
}
}
(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat pageWidth = _cvImagesList.frame.size.width;
float currentPage = _cvImagesList.contentOffset.x / pageWidth;
_pageControl.currentPage = currentPage + 1;
NSLog(#"finishPage: %ld", (long)_pageControl.currentPage);
}
Related
I have a UITableView with cells of different heights and I need to know when they are completely visible or not.
At the moment I am looping through each cell in the list of visible cells to check if it is completely visible every time the view is scrolled . Is this the best approach?
Here's my code:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
CGPoint offset = aScrollView.contentOffset;
CGRect bounds = aScrollView.bounds;
NSArray* cells = myTableView.visibleCells;
for (MyCustomUITableViewCell* cell in cells) {
if (cell.frame.origin.y > offset.y &&
cell.frame.origin.y + cell.frame.size.height < offset.y + bounds.size.height) {
[cell notifyCompletelyVisible];
}
else {
[cell notifyNotCompletelyVisible];
}
}
}
Edit:
Please note that *- (NSArray )visibleCells returns visible cells which are both completely visible and partly visible.
Edit 2:
This is the revised code after combining solutions from both lnafziger and Vadim Yelagin:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
NSArray* cells = myTableView.visibleCells;
NSArray* indexPaths = myTableView.indexPathsForVisibleRows;
NSUInteger cellCount = [cells count];
if (cellCount == 0) return;
// Check the visibility of the first cell
[self checkVisibilityOfCell:[cells objectAtIndex:0] forIndexPath:[indexPaths objectAtIndex:0]];
if (cellCount == 1) return;
// Check the visibility of the last cell
[self checkVisibilityOfCell:[cells lastObject] forIndexPath:[indexPaths lastObject]];
if (cellCount == 2) return;
// All of the rest of the cells are visible: Loop through the 2nd through n-1 cells
for (NSUInteger i = 1; i < cellCount - 1; i++)
[[cells objectAtIndex:i] notifyCellVisibleWithIsCompletelyVisible:YES];
}
- (void)checkVisibilityOfCell:(MultiQuestionTableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath {
CGRect cellRect = [myTableView rectForRowAtIndexPath:indexPath];
cellRect = [myTableView convertRect:cellRect toView:myTableView.superview];
BOOL completelyVisible = CGRectContainsRect(myTableView.frame, cellRect);
[cell notifyCellVisibleWithIsCompletelyVisible:completelyVisible];
}
You can get the rect of a cell with rectForRowAtIndexPath: method and compare it with tableview's bounds rect using CGRectContainsRect function.
Note that this will not instantiate the cell if it is not visible, and thus will be rather fast.
Swift
let cellRect = tableView.rectForRowAtIndexPath(indexPath)
let completelyVisible = tableView.bounds.contains(cellRect)
Obj-C
CGRect cellRect = [tableView rectForRowAtIndexPath:indexPath];
BOOL completelyVisible = CGRectContainsRect(tableView.bounds, cellRect);
Of course this will not regard the table view being clipped by a superview or obscured by another view.
I would change it like this:
- (void)checkVisibilityOfCell:(MyCustomUITableViewCell *)cell inScrollView:(UIScrollView *)aScrollView {
CGRect cellRect = [aScrollView convertRect:cell.frame toView:aScrollView.superview];
if (CGRectContainsRect(aScrollView.frame, cellRect))
[cell notifyCompletelyVisible];
else
[cell notifyNotCompletelyVisible];
}
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
NSArray* cells = myTableView.visibleCells;
NSUInteger cellCount = [cells count];
if (cellCount == 0)
return;
// Check the visibility of the first cell
[self checkVisibilityOfCell:[cells firstObject] inScrollView:aScrollView];
if (cellCount == 1)
return;
// Check the visibility of the last cell
[self checkVisibilityOfCell:[cells lastObject] inScrollView:aScrollView];
if (cellCount == 2)
return;
// All of the rest of the cells are visible: Loop through the 2nd through n-1 cells
for (NSUInteger i = 1; i < cellCount - 1; i++)
[[cells objectAtIndex:i] notifyCompletelyVisible];
}
You can try something like this to see how much percentage is visible:
-(void)scrollViewDidScroll:(UIScrollView *)sender
{
[self checkWhichVideoToEnable];
}
-(void)checkWhichVideoToEnable
{
for(UITableViewCell *cell in [tblMessages visibleCells])
{
if([cell isKindOfClass:[VideoMessageCell class]])
{
NSIndexPath *indexPath = [tblMessages indexPathForCell:cell];
CGRect cellRect = [tblMessages rectForRowAtIndexPath:indexPath];
UIView *superview = tblMessages.superview;
CGRect convertedRect=[tblMessages convertRect:cellRect toView:superview];
CGRect intersect = CGRectIntersection(tblMessages.frame, convertedRect);
float visibleHeight = CGRectGetHeight(intersect);
if(visibleHeight>VIDEO_CELL_SIZE*0.6) // only if 60% of the cell is visible
{
// unmute the video if we can see at least half of the cell
[((VideoMessageCell*)cell) muteVideo:!btnMuteVideos.selected];
}
else
{
// mute the other video cells that are not visible
[((VideoMessageCell*)cell) muteVideo:YES];
}
}
}
}
If you also want to take the contentInset into account, and don't want to rely on a superview (the table view frame in superview could be something else than 0,0), here's my solution:
extension UITableView {
public var boundsWithoutInset: CGRect {
var boundsWithoutInset = bounds
boundsWithoutInset.origin.y += contentInset.top
boundsWithoutInset.size.height -= contentInset.top + contentInset.bottom
return boundsWithoutInset
}
public func isRowCompletelyVisible(at indexPath: IndexPath) -> Bool {
let rect = rectForRow(at: indexPath)
return boundsWithoutInset.contains(rect)
}
}
From the docs:
visibleCells Returns the table cells that are visible in the receiver.
- (NSArray *)visibleCells
Return Value An array containing UITableViewCell objects, each
representing a visible cell in the
receiving table view.
Availability Available in iOS 2.0 and later.
See Also –
indexPathsForVisibleRows
The code below will let you check if a collection view cell is completely visible through the layout attributes of the collection view.
guard let cellRect = collectionView.layoutAttributesForItem(at: indexPath)?.frame else { return }
let isCellCompletelyVisible = collectionView.bounds.contains(cellRect)
Swift 5+
we can use
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
...
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
...
}
let cellRect = tableView.rectForRow(at: indexPath)
let completelyVisible = tableView.bounds.contains(cellRect)
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
CGRect frame = cell.frame;
if (CGRectContainsRect(CGRectOffset(self.collectionView.frame, self.collectionView.contentOffset.x, self.collectionView.contentOffset.y), frame))
{
// is on screen
}
Even if you said you want to check it every time you scrolled, you can also use
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
CGRect cellRect = [tableView convertRect:cell.frame toView:tableView.superview];
if (CGRectContainsRect(tableView.frame, cellRect)){
//Do things in case cell is fully displayed
}
}
- (BOOL)checkVisibilityOfCell{
if (tableView.contentSize.height <= tableView.frame.size.height) {
return YES;
} else{
return NO;
}
}
Maybe for this issue better used next function from UITableViewDelegate
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
I am embeding JBParallaxCell, a UITableViewCell subclass. I want to call a function:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Get visible cells on table view.
NSArray *visibleCells = [self.tableView visibleCells];
for (JBParallaxCell *cell in visibleCells) {
[cell cellOnTableView:self.tableView didScrollOnView:self.view];
}
}
I converted this code to Swift:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleCells = table.visibleCells
var cells : JBParallaxCell?
for cells in visibleCells {
cells(on: table, didScrollOn: self.view)
// cells.cellOnTableView(tableView: table, didScrollOn: self.view)
}
}
They give error call not function of UITableViewCell
If your tableview outlet is called table, then you'd could do:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in table.visibleCells {
if let cell = cell as? JBParallaxCell {
cell.cell(on: table, didScrollOn: view)
}
}
}
Or, equivalent:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in table.visibleCells {
(cell as? JBParallaxCell)?.cell(on: table, didScrollOn: view)
}
}
You need to convert [cell cellOnTableView:self.tableView didScrollOnView:self.view]; to swift and add it in JBParallaxCell.
I converted it myself
func cellOnTableView(tableView: UITableView, didScrollOn view: UIView) {
let rectInSuperview: CGRect = tableView.convert(frame, to: view)
let distanceFromCenter: Float = Float(frame.height / 2 - rectInSuperview.minY)
let difference: Float = Float(parallaxImage.frame.height - frame.height);
let move: Float = (distanceFromCenter / Float(view.frame.height)) * difference
var imageRect: CGRect = parallaxImage.frame
imageRect.origin.y = CGFloat(move - (difference / 2))
self.parallaxImage.frame = imageRect
}
And change this line let visibleCells = table.visibleCells to
if let visibleCells = table.visibleCells as? JBParallaxCell
I would like to set the UITableView to match the height for all the contents in the table view.
This is my storyboard
The problem with this is the top and bottom ImageView is always static on the screen.
The there are suppose to be 10 items on the table view but only 7 shows up due to screen size limitation. I would like to show all 10 before user is able to see the bottom ImageView. (btw, all 3 of the views ie. both the image views and tableview is in a uiscrollview)
IDEAL
Some of the other limitations that i have to work with is that the number of items in the table view is dynamic meaning it can be in any amount of usually less than 10 that i will later retrieve from an api. And the cell height is also dynamic depending on the contents.
I have only just started with some simple code
class ExampleViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var items: [String] = [
"Item 01", "Item 02", "Item 03", "Item 04", "Item 05",
"Item 06", "Item 07", "Item 08", "Item 09", "Item 10"]
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
cell.textLabel?.text = self.items[indexPath.row]
return cell
}
}
Subclass your UITableView to override the intrinsicContentSize to be its contentSize, like this:
override var intrinsicContentSize: CGSize {
return contentSize
}
Then use automatic row heights for your table, so your exampleViewController's viewDidLoad would have:
tableView.estimatedRowHeight = 44
And the UITableViewDelegate function:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
When you receive data from your API and reload your table, just call:
tableView.invalidateIntrinsicContentSize()
This will tell your table to resize itself to the same size as its contents (because of the override), and move your bottom image as needed.
If your storyboard throws an error saying that your UIScrollView has an ambiguous height because there's no height constraint on the UITableView, select your UITableView and give it a placeholder intrinsic size in the Size Inspector.
The answers using the subclassing technique are incomplete. You should also override layoutSubviews() like this.
public class DynamicSizeTableView: UITableView
{
override public func layoutSubviews() {
super.layoutSubviews()
if bounds.size != intrinsicContentSize {
invalidateIntrinsicContentSize()
}
}
override public var intrinsicContentSize: CGSize {
return contentSize
}
}
This is what I utilize in production apps:
Swift 5, 2021
import UIKit
class DynamicTableView: UITableView {
/// Will assign automatic dimension to the rowHeight variable
/// Will asign the value of this variable to estimated row height.
var dynamicRowHeight: CGFloat = UITableView.automaticDimension {
didSet {
rowHeight = UITableView.automaticDimension
estimatedRowHeight = dynamicRowHeight
}
}
public override var intrinsicContentSize: CGSize { contentSize }
public override func layoutSubviews() {
super.layoutSubviews()
if !bounds.size.equalTo(intrinsicContentSize) {
invalidateIntrinsicContentSize()
}
}
}
You need to set an IBOutlet to the NSLayoutConstraint that sets the tableView height (first you need create the height constraint with any value, doesn't matter) and then ctrl drag it to your class file
Then in your viewWillAppear you have to calculate the tableView height and set it. Like this:
var tableViewHeight:CGFloat = 0;
for (var i = tableView(self.tableView , numberOfRowsInSection: 0) - 1; i>0; i-=1 ){
tableViewHeight = height + tableView(self.tableView, heightForRowAtIndexPath: NSIndexPath(forRow: i, inSection: 0) )
}
tableViewHeightLayout.constant = tableViewHeight
And that's pretty much it. That will give your scrollView content size and shouldn't raise any warnings.
Update Swift 4
this code working be good
self.scrollView.layoutIfNeeded()
self.view.layoutIfNeeded()
self.tableViewHeightConstraint.constant = CGFloat(self.tableView.contentSize.height)
You probably have to implement the table view intrinsic content size. Please check this answer to see if it helps.
I remember having this problem and even created a custom UITableView subclass.
#import "IntrinsicTableView.h"
#implementation IntrinsicTableView
#pragma mark - UIView
- (CGSize)intrinsicContentSize
{
return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}
#pragma mark - UITableView
- (void)endUpdates
{
[super endUpdates];
[self invalidateIntrinsicContentSize];
}
- (void)reloadData
{
[super reloadData];
[self invalidateIntrinsicContentSize];
}
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super reloadSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super insertRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super insertSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super deleteSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
#end
Update for Swift 5. Adding maxHeight so that you can specify how tall you want your tableView to be
class SelfSizingTableView: UITableView {
var maxHeight = CGFloat.infinity
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
}
}
override var intrinsicContentSize: CGSize {
let height = min(maxHeight, contentSize.height)
return CGSize(width: contentSize.width, height: height)
}
}
In that case, don't make your bottom cell static, make it a part of table view and insert this bottom image in last row using table view delegate method - insertRowAtIndexPath
In this type of case add your bottom imageView(red) in a table footer view.
To add footer view in UITableView you can use:
tableViewObj.tableFooterView = footerViewObj;
Try this also
in ViewDidLoad
self.table.estimatedRowHeight = 44.0 ;
self.table.rowHeight = UITableViewAutomaticDimension;
Height for row at index path
-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewAutomaticDimension;}
Easy way here.
Step 1: Set a height constraint for the table view
Step 2: Control drag the constraint
Step 3: Before you return the count of the rows. In numberOfRowsInSection method, do
tableViewHeightConstraint.constant = tableView.rowHeight * CGFloat(someArray.count)
Of course you can edit the height anchor programmatically, the logic here is to adjust the table view height according to the cell height and cell number.
Based on solution #nikans, written in Xamarin
[Register(nameof(DynamicSizeTableView)), DesignTimeVisible(true)]
public class DynamicSizeTableView : UITableView
{
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (Bounds.Size != IntrinsicContentSize)
InvalidateIntrinsicContentSize();
}
public override CGSize IntrinsicContentSize => ContentSize;
public DynamicSizeTableView(CGRect frame) : base(frame) { }
public DynamicSizeTableView(IntPtr handle) : base(handle) { }
}
Here is the simplest Solution
First Give a height to the tableView.
Create outlet of that height in view Controller. let's say tableViewHeight
Then do this in viewDidLoad or where you populate the data after calling tableView.reloadData()
var height = 0.0
for i in 0..<items.count {
let frame = tableView.rectForRow(at: IndexPath(row: i, section: 0))
height += frame.size.height
}
tableViewHeight.constant = height
This Also Works with tableViews that have dynamic cell heights
Based on solution of #rr1g0
Updated for Swift 5 in 2020, and works with TableViews with sections too.
Create height constraint for tableView and create an outlet to it. And in viewDidLayoutSubviews() use the code below:
var tableViewHeight: CGFloat = 0
for section in 0..<tableView.numberOfSections {
for row in 0..<tableView.numberOfRows(inSection: section) {
tableViewHeight += tableView(tableView, heightForRowAt: IndexPath(row: row, section: section))
}
}
tableViewHeightConstraint.constant = tableViewHeight
I have a vertically scrolling UICollectionView that uses a subclass of UICollectionViewFlowLayout to try and eliminate inter-item spacing. This would result in something that looks similar to a UITableView, but I need the CollectionView for other purposes. There is a problem in my implementation of the FlowLayout subclass that causes cells to disappear when scrolling fast. Here is the code for my FlowLayout subclass:
EDIT: See Comments For Update
class ListLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
if var answer = super.layoutAttributesForElementsInRect(rect) {
for attr in (answer as [UICollectionViewLayoutAttributes]) {
let ip = attr.indexPath
attr.frame = self.layoutAttributesForItemAtIndexPath(ip).frame
}
return answer;
}
return nil
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let currentItemAtts = super.layoutAttributesForItemAtIndexPath(indexPath) as UICollectionViewLayoutAttributes
if indexPath.item == 0 {
var frame = currentItemAtts.frame
frame.origin.y = 0
currentItemAtts.frame = frame
return currentItemAtts
}
let prevIP = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIP).frame
let prevFrameTopPoint = prevFrame.origin.y + prevFrame.size.height
var frame = currentItemAtts.frame
frame.origin.y = prevFrameTopPoint
currentItemAtts.frame = frame
return currentItemAtts
}
}
One other thing to note: My cells are variable height. Their height is set by overriding preferredLayoutAttributesFittingAttributes in the subclass of the custom cell:
override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes! {
let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as UICollectionViewLayoutAttributes
attr.frame.size = CGSizeMake(self.frame.size.width, myHeight)
return attr
}
And I set the layout's estimated size on initialization:
flowLayout.estimatedItemSize = CGSize(width: UIScreen.mainScreen().bounds.size.width, height: 60)
Here is a GIF that demonstrates this problem:
Does anybody have an idea as to what's going on? Your help is much appreciated.
Thanks!
I have been struggling with this assignment for quite some time now. What I would like to develop is a scrollview or collectionview which scrolls continuously both vertical and horizontal.
Here is an image of how I think this should look like. The transparent boxes are the views/cells which are re-loaded from the memory. As soon as a view/cell gets outside of the screen, it should be reused for upcoming new cell.. just like how a UITableViewController works.
I know that a UICollectionView can only be made to infinite scroll horizontal OR vertical, not both. However, I don't know how to do this using a UIScrollView.
I tried the code attached to an answer on this question and I can get it to re-create views (e.g. % 20) but that's not really what I need.. besides, its not continuous.
I know it is possible, because the HBO Go app does this.. I want exactly the same functionality.
My Question: How can I achieve my goal? Are there any guides/tutorials that can show me how? I can't find any.
You can get infinite scrolling, by using the technique of re-centering the UIScrollView after you get a certain distance away from the center. First, you need to make the contentSize big enough that you can scroll a bit, so I return 4 times the number of items in my sections and 4 times the number of sections, and use the mod operator in the cellForItemAtIndexPath method to get the right index into my array. You then have to override layoutSubviews in a subclass of UICollectionView to do the re-centering (this is demonstrated in the WWDC 2011 video, "Advanced Scroll View Techniques"). Here is the controller class that has the collection view (set up in IB) as a subview:
#import "ViewController.h"
#import "MultpleLineLayout.h"
#import "DataCell.h"
#interface ViewController ()
#property (weak,nonatomic) IBOutlet UICollectionView *collectionView;
#property (strong,nonatomic) NSArray *theData;
#end
#implementation ViewController
- (void)viewDidLoad {
self.theData = #[#[#"1",#"2",#"3",#"4",#"5"], #[#"6",#"7",#"8",#"9",#"10"],#[#"11",#"12",#"13",#"14",#"15"],#[#"16",#"17",#"18",#"19",#"20"]];
MultpleLineLayout *layout = [[MultpleLineLayout alloc] init];
self.collectionView.collectionViewLayout = layout;
self.collectionView.showsHorizontalScrollIndicator = NO;
self.collectionView.showsVerticalScrollIndicator = NO;
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.view.backgroundColor = [UIColor blackColor];
[self.collectionView registerClass:[DataCell class] forCellWithReuseIdentifier:#"DataCell"];
[self.collectionView reloadData];
}
- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section {
return 20;
}
- (NSInteger)numberOfSectionsInCollectionView: (UICollectionView *)collectionView {
return 16;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
DataCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"DataCell" forIndexPath:indexPath];
cell.label.text = self.theData[indexPath.section %4][indexPath.row %5];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
// UICollectionViewCell *item = [collectionView cellForItemAtIndexPath:indexPath];
NSLog(#"%#",indexPath);
}
Here is the UICollectionViewFlowLayout subclass:
#define space 5
#import "MultpleLineLayout.h"
#implementation MultpleLineLayout { // a subclass of UICollectionViewFlowLayout
NSInteger itemWidth;
NSInteger itemHeight;
}
-(id)init {
if (self = [super init]) {
itemWidth = 60;
itemHeight = 60;
}
return self;
}
-(CGSize)collectionViewContentSize {
NSInteger xSize = [self.collectionView numberOfItemsInSection:0] * (itemWidth + space); // "space" is for spacing between cells.
NSInteger ySize = [self.collectionView numberOfSections] * (itemHeight + space);
return CGSizeMake(xSize, ySize);
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path {
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
attributes.size = CGSizeMake(itemWidth,itemHeight);
int xValue = itemWidth/2 + path.row * (itemWidth + space);
int yValue = itemHeight + path.section * (itemHeight + space);
attributes.center = CGPointMake(xValue, yValue);
return attributes;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSInteger minRow = (rect.origin.x > 0)? rect.origin.x/(itemWidth + space) : 0; // need to check because bounce gives negative values for x.
NSInteger maxRow = rect.size.width/(itemWidth + space) + minRow;
NSMutableArray* attributes = [NSMutableArray array];
for(NSInteger i=0 ; i < self.collectionView.numberOfSections; i++) {
for (NSInteger j=minRow ; j < maxRow; j++) {
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:j inSection:i];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
}
return attributes;
}
And finally, here is the subclass of UICollectionView:
-(void)layoutSubviews {
[super layoutSubviews];
CGPoint currentOffset = self.contentOffset;
CGFloat contentWidth = self.contentSize.width;
CGFloat contentHeight = self.contentSize.height;
CGFloat centerOffsetX = (contentWidth - self.bounds.size.width)/ 2.0;
CGFloat centerOffsetY = (contentHeight - self.bounds.size.height)/ 2.0;
CGFloat distanceFromCenterX = fabsf(currentOffset.x - centerOffsetX);
CGFloat distanceFromCenterY = fabsf(currentOffset.y - centerOffsetY);
if (distanceFromCenterX > contentWidth/4.0) { // this number of 4.0 is arbitrary
self.contentOffset = CGPointMake(centerOffsetX, currentOffset.y);
}
if (distanceFromCenterY > contentHeight/4.0) {
self.contentOffset = CGPointMake(currentOffset.x, centerOffsetY);
}
}
#updated for swift 3 and changed how the maxRow is calculated otherwise the last column is cutoff and can cause errors
import UIKit
class NodeMap : UICollectionViewController {
var rows = 10
var cols = 10
override func viewDidLoad(){
self.collectionView!.collectionViewLayout = NodeLayout(itemWidth: 400.0, itemHeight: 300.0, space: 5.0)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return rows
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return cols
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "node", for: indexPath)
}
}
class NodeLayout : UICollectionViewFlowLayout {
var itemWidth : CGFloat
var itemHeight : CGFloat
var space : CGFloat
var columns: Int{
return self.collectionView!.numberOfItems(inSection: 0)
}
var rows: Int{
return self.collectionView!.numberOfSections
}
init(itemWidth: CGFloat, itemHeight: CGFloat, space: CGFloat) {
self.itemWidth = itemWidth
self.itemHeight = itemHeight
self.space = space
super.init()
}
required init(coder aDecoder: NSCoder) {
self.itemWidth = 50
self.itemHeight = 50
self.space = 3
super.init()
}
override var collectionViewContentSize: CGSize{
let w : CGFloat = CGFloat(columns) * (itemWidth + space)
let h : CGFloat = CGFloat(rows) * (itemHeight + space)
return CGSize(width: w, height: h)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let x : CGFloat = CGFloat(indexPath.row) * (itemWidth + space)
let y : CGFloat = CGFloat(indexPath.section) + CGFloat(indexPath.section) * (itemHeight + space)
attributes.frame = CGRect(x: x, y: y, width: itemWidth, height: itemHeight)
return attributes
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let minRow : Int = (rect.origin.x > 0) ? Int(floor(rect.origin.x/(itemWidth + space))) : 0
let maxRow : Int = min(columns - 1, Int(ceil(rect.size.width / (itemWidth + space)) + CGFloat(minRow)))
var attributes : Array<UICollectionViewLayoutAttributes> = [UICollectionViewLayoutAttributes]()
for i in 0 ..< rows {
for j in minRow ... maxRow {
attributes.append(self.layoutAttributesForItem(at: IndexPath(item: j, section: i))!)
}
}
return attributes
}
}
#rdelmar's answer worked like a charm, but I needed to do it in swift. Here's the conversion :)
class NodeMap : UICollectionViewController {
#IBOutlet var activateNodeButton : UIBarButtonItem?
var rows = 10
var cols = 10
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return rows
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return cols
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("node", forIndexPath: indexPath)
}
override func viewDidLoad() {
self.collectionView!.collectionViewLayout = NodeLayout(itemWidth: 100.0, itemHeight: 100.0, space: 5.0)
}
}
class NodeLayout : UICollectionViewFlowLayout {
var itemWidth : CGFloat
var itemHeight : CGFloat
var space : CGFloat
init(itemWidth: CGFloat, itemHeight: CGFloat, space: CGFloat) {
self.itemWidth = itemWidth
self.itemHeight = itemHeight
self.space = space
super.init()
}
required init(coder aDecoder: NSCoder) {
self.itemWidth = 50
self.itemHeight = 50
self.space = 3
super.init()
}
override func collectionViewContentSize() -> CGSize {
let w : CGFloat = CGFloat(self.collectionView!.numberOfItemsInSection(0)) * (itemWidth + space)
let h : CGFloat = CGFloat(self.collectionView!.numberOfSections()) * (itemHeight + space)
return CGSizeMake(w, h)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
let x : CGFloat = CGFloat(indexPath.row) * (itemWidth + space)
let y : CGFloat = CGFloat(indexPath.section) + CGFloat(indexPath.section) * (itemHeight + space)
attributes.frame = CGRectMake(x, y, itemWidth, itemHeight)
return attributes
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
let minRow : Int = (rect.origin.x > 0) ? Int(floor(rect.origin.x/(itemWidth + space))) : 0
let maxRow : Int = Int(floor(rect.size.width/(itemWidth + space)) + CGFloat(minRow))
var attributes : Array<UICollectionViewLayoutAttributes> = [UICollectionViewLayoutAttributes]()
for i in 0...self.collectionView!.numberOfSections()-1 {
for j in minRow...maxRow {
attributes.append(self.layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: j, inSection: i)))
}
}
return attributes
}
}
Resetting the contentOffset probably is the best solution figured out so far.
A few steps should be taken to achieve this:
Pad extra items at both the left and right side of the original data set to achieve larger scrollable area; This is similar to having a large duplicated data set, but difference is the amount;
At start, the collection view’s contentOffset is calculated to show only the original data set (drawn in black rectangles);
When the user scrolls right and contentOffset hits the trigger value, we reset contentOffset to show same visual results; but actually different data; When the user scrolls left, the same logic is used.
So, the heavy lifting is in calculating how many items should be padded both on the left and right side. If you take a look at the illustration, you will find that a minimum of one extra screen of items should be padded on left and also, another extra screen on the right. The exact amount padded depends on how many items are in the original data set and how large your item size is.
I wrote a post on this solution:
https://github.com/Alex1989Wang/Blogs/blob/master/contents/2018-03-24-Infinite-Scrolling-and-the-Tiling-Logic.md