I am using tableView:heightForRowAtIndexPath: to set a custom height for each table cell in a UITableView. This is an expensive operation but I have not found a way to speed it up yet (In the future I will be trying some of the methods mentioned by taber).
For now I would just like to display a loading screen of some sort. it does not need to be animated, as long as it is visible while the table view is being loaded. Is this possible?
EDIT: I have tried the loading screen method mentioned in taber's answer, but since it runs in the same thread as the UITableView it does not appear until the table view is finished loading and thus not very useful in this situation :)
Try #3:
You can add an activity indicator as a new view on top of your table view... something like: http://tapadoo.com/2009/iphone-how-to-do-full-screen-activity-status - then the trick would be figuring out when it's actually finished. I guess in your heightForRowAtIndexPath: method you could keep some kind of row count and check if that count is >= your data model row count. Does that do the trick?
Try #2:
Try to comment out your overridden tableView:heightForRowAtIndexPath: method completely, and instead where you create your cells in cellForRowAtIndexPath: try something like this:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CGRect contentRectTall = CGRectMake(0.0, 0.0, 302.0, 140.0);
CGRect contentRectMed = CGRectMake(0.0, 0.0, 302.0, 70.0);
CGRect contentRectSmall = CGRectMake(0.0, 0.0, 302.0, 42.0);
... cell dequeue etc ...
if ( ...the type of this cell is kRowTypeTall for example... ) {
cell = [[[UITableViewCell alloc] initWithFrame:contentRectTall reuseIdentifier:CellIdentifier] autorelease];
...
} else if ( ...the cell type is kRowTypeMed ) {
...
} else if ( ...the cell type is kRowTypeSmall ) {
...
}
... other cell customization ...
}
Other than that, you might want to look into "lazy loading" where you'd load say the first 50 cells, then add a "load 50 more" type of button at the end.
If you already know your row type (which is contained in some kind of data source) it should be pretty quick to pull out.
Something like...
enum {
kRowTypeHuge = 0,
kRowTypeMed,
kRowTypeSmall
};
-(id)init {
...
NSArray *tableRows = [[NSArray alloc] initWithObjects:
[NSNumber numberWithUnsignedInt: 0],
[NSNumber numberWithUnsignedInt: 1],
[NSNumber numberWithUnsignedInt: 0],
[NSNumber numberWithUnsignedInt: 0],
[NSNumber numberWithUnsignedInt: 2],
nil
];
self.rows = tableRows;
[tableRows release];
...
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger rowType = [[[self.rows] objectAtIndex: indexPath.row] unsignedIntValue];
if(rowType == kRowTypeHuge) {
return 105.0;
} else if(rowType == kRowTypeMed) {
return 44.0;
}
return 20.0;
}
To be positive that heightForRowAtIndexPath is the issue... does something like this render super fast?
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 52.0;
}
Related
I created a UITableView that contains custom section header views. Now, I want it to display a bit more data on the uppermost current visible section. I plan to use the event scrollViewDidEndDecelerating to update the section headers. Currently, the problem is that I cannot set the section header height for a specific section number.
I did try using heightForHeaderInSection beforehand, but the app just crashes with the following output:
'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array'
I was using the code:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if (tableView == self.tableView)
{
NSArray *visibleCells = [self.tableView visibleCells];
NSMutableArray *visibleSections = [[NSMutableArray alloc] init];
for (NSInteger index = 0; index < [visibleCells count]; index++)
{
UITableViewCell *currentCell = [visibleCells objectAtIndex:index];
NSIndexPath *currentPath = (NSIndexPath *)[self.tableView indexPathForCell:currentCell];
if (![visibleSections containsObject:[NSNumber numberWithInt:currentPath.section]])
{
[visibleSections addObject:[NSNumber numberWithInt:currentPath.section]];
NSLog([NSString stringWithFormat:#"%ld", (long)[visibleSections count]]);
[visibleSections sortedArrayUsingDescriptors:[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:nil ascending:YES]]];
}
}
if (visibleSections == nil)
{
return 42.0;
}
else if ([[visibleSections objectAtIndex:0] integerValue] == section)
{
return 58.0;
}
else
{
return 42.0;
}
}
}
I couldn't quite work out what went wrong in my heightForHeaderInSection method, but I knew it had something to do with the NSMutableArray, visibleSections.
Any hints or answers as to how I can go about changing the height for a specific section header view outside of heightForHeaderInSection and/or how I can fix my code above would be really helpful.
Edit:
Just to make the solution to my crashing problem a bit clearer, if (visibleSections == nil) should not be used in place of if ([visibleSections count] < 1) or if ([visibleSections count] == 0).
I think you could also do it like this, if you want the first section header to be taller when the table first appears (topSection is an NSInteger property):
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
self.topSection = ((NSIndexPath *)[self.tableView indexPathsForVisibleRows][0]).section;
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:self.topSection] withRowAnimation:NO];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (self.topSection == section)
{
return 58.0;
}
else
{
return 42.0;
}
}
OK so it turns out this is a harder problem than it first seems. The best I have come up with so far is
Don't treat the header that is for the top section any different and populate them all with the extra data.
You can show and hide different parts by being clever with positioning the "additional" items so that they will be outside of the parent view's bounds when it is smaller and making the parent view clipToBounds.
Failing that you can make a custom UIView subclass and do some manipulation in layoutSubviews
The end implementation I was settling on was this
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
self.topSection = [indexPaths count] ? [indexPaths[0] section] : -1;
if (indexPaths.count > 1) {
self.topSection = [indexPaths[1] section];
}
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
{
if (section <= self.topSection) {
return 60;
} else {
return 20;
}
}
It's by no means perfect but it looked semi reasonable and could be tweaked.
Things to note:
You may need to assess if there is too much work going on in scrollViewDidScroll: but it didn't appear to cause any lag for me (I've not really tested properly)
I set the top section using the second indexPath if available as it looked slightly more pleasing/less clunky
I use section <= self.topSection because the header's before are all of screen so there is no point in reducing the size of them which causes really clunky animation.
So after trying this you may need to dig deeper or want to rethink your design a little
You cannot directly refer to the arrays first object by calling objectAtIndex:0, you gotta stay defensive so change this:
else if ([[visibleSections objectAtIndex:0] integerValue] == section)
{
return 58.0;
}
To
else if([visibleSections count]>0)
{
if ([[visibleSections objectAtIndex:0] integerValue] == section)
{
return 58.0;
}
}
Try changing this line:
NSMutableArray *visibleSections = [[NSMutableArray alloc] init]
to:
NSMutableArray *visibleSections = [NSMutableArray array];
Which initializes the array.
I have a UITableView with 4 UILabel's: Title, Body, Author and Date, he looks like this:
What I want to accomplish is, when user click on the cell itself, another label should be added to the cell, the "Body" label and the cell should expand according to this label size.
Something like this:
How can I do that? I've searched stackoverflow, tried some code pieces, but still didn't found the right solution.
Thanks!
Edit 1: 14.11.12 at 14:52
I managed to change the size of the UILabel with the current text:
- (CGRect )resizeLabelByFontSize:(UILabel *)customCellLabel withMaxHeightSize:(CGFloat )maxHeight
{
CGSize maximumLabelSize = CGSizeMake(239, maxHeight);
CGSize expectedLabelSize = [customCellLabel.text sizeWithFont:customCellLabel.font constrainedToSize:maximumLabelSize lineBreakMode:customCellLabel.lineBreakMode];
//adjust the label the the new height.
CGRect newFrame = customCellLabel.frame;
newFrame.size.height = expectedLabelSize.height;
return newFrame;
}
But how can I change the size of the cell according to the size of the new UILabel?
By seeing Images in Question
Here is the method which just create the Dynamic FRAME for UILabel have a look at this
By getting the Height and Width for UIlabel you can calculate the Whole height and could set the Row Height of UITableView.
- (void)setLabeltextWithVerticalAlignTop:(NSString *)theText
{
CGSize labelSize;
// here labelSize is hard-wired but could use constants to populate the size
labelSize = CGSizeMake(210, 129);//this is just for example
//now create the Size from textString SO that We could assign this size to the Label.
CGSize theStringSize = [theText sizeWithFont:lblTitle.font constrainedToSize:labelSize lineBreakMode:lblTitle.lineBreakMode];
lblTitle.frame = CGRectMake(lblTitle.frame.origin.x, lblTitle.frame.origin.y, theStringSize.width, theStringSize.height);
lblTitle.text = theText;
}
Call Above Method For setting the height and Width of description Label you need to pass the text to be shown on that description label.
As you gets the height for that Label, Now On the Basis of this You can Adjust the heigh of Row of TableView.
EDIT:Above Code Just Create the Dynamic Frame For The UILabel
You should take a view of this this is what you looking for....!!!.here you would find a sample code too.
EDIT:As you edited your Question see ,it just the logic which you need to convert it into runnable code here it is.
Use Below Method in Your Code called for each row, and make some calculation inside it.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat rowHeight=0.0;
//here it seems cell have 4 subview added on it.
//so if you could calculate the totla hieht of them.
//so what you really need to do.you just use hieght calculative Method for getting hieght of each of three UILabel
//you need to modify `setLabeltextWithVerticalAlignTop` method .
rowHeight= [self setLabeltextWithVerticalAlignTop:#"pass the correspondingText"];// suppose it returns some hieght for FisrtLabel.
//suppoose here you get the 20.0 height here
rowHeight= rowHeight+[self setLabeltextWithVerticalAlignTop:#"pass the correspondingText"];
// suppose it returns some hieght for secondUIlabel.
//suppoose here you get the 40.0 height here
rowHeight= rowHeight+ [self setLabeltextWithVerticalAlignTop:#"pass the correspondingText"];
// suppose it returns some hieght for ThirdUIlabel.
// suppoose here you get the 15.0 height here
//here you have totla height you just need to add some gapping floating value for all of three UIlabel.so that the could not overlap like as.
rowHeight= rowHeight+20.0;
//now you can return that total height
return rowHeight;
}
Note:This is just logic you need to convert it into runnable code.i am sure this can help.
I hope it may help you.
Implement the following methods
– (void) tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// cast cell, add label, expand labels etc
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [indexPath isEqualTo:[tableView indexPathForSelectedRow]] ? /* expanded height */ : 80 /* normal height */;
}
If you want the row to stay selected even after another row is selected then add a custom BOOL property to your custom cell, e.g. expanded, and use that to determine the height.
You can use tableView:didSelectRowAtIndexPath
In that method you can then create your code to unhide the body label, adjust the relative positions of everything else. Calculate the new height of the row and then call the Table View's reloadRowsAtIndexPath: withRowAnimation: method.
Sorry if there's not a lot of detail in that, but hopefully that should get you on the right track.
Ok, firstly... To expand you need something like this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
Now here is the catch:
You should calculate the size of the UITableViewCell (expanded and non-expanded)
Doing so when you are actually scrolling might be expensive and will give you a bad experience
My advice:
Calculate both sides, before you have actually conclude to build the UITableView, since you want to have dynamic sizes. If you don't and all cells will have the same size expanded, you can use what lammmert said.
NSIndexPath *selectedRow;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
selectedRow = indexPath;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
if(indexPath == selectedRow){
//return your custom value
}
return 100;
}
I think it will look something like that
So, in order to do this, using expended UITableViewCell, i've created 2 different custom cells, at start the table is showing the first cell, when I click on the cell, the table is showing the second one. Its that easy - yeah!
So I have the UIViewController with the UITableView that implements the table delegate methods:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if([self.selectedCellIndexPath isEqual:indexPath])
{
return [self expandedCellHeight:indexPath];
}
else
{
return kRegularCellHeight;
}
}
-(CGFloat)expandedCellHeight:(NSIndexPath *)indexPath
{
CGSize maxSize = CGSizeMake(303, 200);
NSString* bodyText = [[self.data objectAtIndex:indexPath.row] objectForKey:kForumMessagesBody];
CGSize fitSize = [bodyText sizeWithFont:[UIFont systemFontOfSize:13] constrainedToSize:maxSize lineBreakMode:UILineBreakModeWordWrap];
CGFloat height = 384 - 69 + fitSize.height;
NSLog(#"expandedHeight: %f",height);
return height;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Answer cell
if ([self.selectedCellIndexPath isEqual:indexPath])
{
cell = [tableView dequeueReusableCellWithIdentifier:[ForumCell expandedAnswerReuseIdentifier]];
if (cell == nil)
{
cell = [ForumCell expandedAnswerCell];
}
self.expandedCell = cell;
}
else
{
cell = [tableView dequeueReusableCellWithIdentifier:[ForumCell reqularAnswerReuseIdentifier]];
if (cell == nil)
{
cell = [ForumCell regularAnswerCell];
}
}
cell.labelMedia.text = [self.data objectAtIndex:indexPath.row];
return cell;
}
I also have custom cell, the class called ForumCell.h and ForumCell.m and it has 2 different XIB files: ForumRegularAnswerCell.xib and ForumExpandedAnswerCell.xib, I have the following code inside ForumCell.h:
+ (NSString*)reqularAnswerReuseIdentifier
{
return #"RegularAnswerCellReuseIdentifier";
}
+ (NSString*)expandedAnswerReuseIdentifier
{
return #"ExpandedAnswerCellReuseIdentifier";
}
+ (ForumCell*)regularAnswerCell
{
NSArray* objs = [[NSBundle mainBundle] loadNibNamed:#"ForumRegularAnswerCell" owner:self options:nil];
ForumCell* result = [objs objectAtIndex:0];
return result;
}
+ (ForumCell*)expandedAnswerCell
{
NSArray* objs = [[NSBundle mainBundle] loadNibNamed:#"ForumExpandedAnswerCell" owner:self options:nil];
ForumCell* result = [objs objectAtIndex:0];
return result;
}
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super initWithCoder:decoder];
if (self)
{
_originalCellHeight = self.frame.size.height;
_originalLblBodyHeight = self.lblBody.frame.size.height;
}
return self;
}
You can also use more than 2 xibs if you'd like its up to you. but this is the basics.
Enjoy!
How do I do an infinite scrolling in a UITableView? I know how to do it using a UIScrollView, in which apple has demonstrated in one of the WWDC's video. I tried doing the following in tableView:cellForRowAtIndexPath::
if (indexPath.row == [self.newsFeedData_ count] - 1)
{
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
but this fails. Any other idea?
If you need to know when you hit the bottom of the UITableView, become it's delegate (because it is a subclass of UIScrollView), and use the -scrollViewDidScroll: delegate method to compare the table's content height and it's actual scroll position.
EDIT (something like this):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat actualPosition = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
if (actualPosition >= contentHeight) {
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
}
You can support infinite scroll with pull to refresh at the top and/or scroll continuously at the bottom with a spinner wheel using:
https://github.com/samvermette/SVPullToRefresh
SVPullToRefresh handles the logic when UITableView reaches the bottom. A spinner is shown automatically and a callback block is fired. You add in your business logic to the callback block.
Here's an example:
#import "UIScrollView+SVInfiniteScrolling.h"
// ...
[tableView addInfiniteScrollingWithActionHandler:^{
// append data to data source, insert new cells at the end of table view
// call [tableView.infiniteScrollingView stopAnimating] when done
}];
This project can be added to your project using CocoaPods or directly compiled into your project.
Here's a very quick and complete demo of an infinite scrolling UITableView I put together...
#interface InfiniteScrollViewController ()
#property (nonatomic) NSMutableArray *tableViewData;
#property (nonatomic) BOOL loadingMoreTableViewData;
#end
#implementation InfiniteScrollViewController
- (void)viewDidLoad {
self.tableViewData = [[NSMutableArray alloc] init];
[self addSomeMoreEntriesToTableView];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.tableViewData.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (indexPath.row < self.tableViewData.count) {
cell.textLabel.text = [self.tableViewData objectAtIndex:indexPath.row];
} else {
cell.textLabel.text = #"Loading more data...";
// User has scrolled to the bottom of the list of available data so simulate loading some more if we aren't already
if (!self.loadingMoreTableViewData) {
self.loadingMoreTableViewData = YES;
[self performSelector:#selector(addSomeMoreEntriesToTableView) withObject:nil afterDelay:5.0f];
}
}
return cell;
}
- (void)addSomeMoreEntriesToTableView {
int loopTill = self.tableViewData.count + 20;
while (self.tableViewData.count < loopTill) {
[self.tableViewData addObject:[NSString stringWithFormat:#"%i", self.tableViewData.count]];
};
self.loadingMoreTableViewData = NO;
[self.tableView reloadData];
}
#end
'UITableView' is same as 'UIScrollView' in 'scrollViewDidScroll' method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualising joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit the method when data count is say for example less than 9.
For me worked better scrollViewDidEndDragging: than scrollViewDidScroll:.
The second approach will send you each position during scroll and cause, if you are fetching remote resources you will hit your endpoint several times, which is not good.
Complete example based on #codafi solution with comments from #danielgomezrico about how to calculate contentHeight:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate {
CGFloat actualPosition = scrollView.contentOffset.y;
CGFloat contentHeight = scrollView.contentSize.height - (self.tableView.frame.size.height);
if (actualPosition >= contentHeight) {
// fetch resources
[self.tableView reloadData];
}
}
Generally I override scrollViewDidEndDecelerating and inside it I put my code to request more data.
Example:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height){
//put here your code
}
}
Recently I uploaded on GitHub a subclass of UITableView, that implements the infinite scroll.
You can download it here:
https://github.com/alchimya/iOS-LazyTableView
rather than overriding we can do this optimally in layoutSubviews.
Here's how I got it implemented. You can get to know more about the implementation here
- (void)layoutSubviews{
[super layoutSubviews];
if(self.delegateForViews){
CGPoint contentOffset = self.contentOffset;
if([self.delegateForViews noOfViews]>numOfReusableViews){
NSUInteger centerIndex=visibleViews.count/2;
NSUInteger noOfViews=[self.delegateForViews noOfViews];
UIView *centerView=[visibleViews objectAtIndex:centerIndex];
CGPoint centerViewOrigin=centerView.frame.origin;
CGSize centerViewSize=centerView.frame.size;
CGFloat offsetDifference=contentOffset.x-centerViewOrigin.x;
CGFloat offsetDifferenceAbs=fabs(contentOffset.x-centerViewOrigin.x);
if(offsetDifferenceAbs>=centerViewSize.width){
if(offsetDifference<0){
currentPosition--;
}else{
currentPosition++;
}
self.contentOffset=centerViewOrigin;
currentPosition=[self getPosition:currentPosition noOfViews:noOfViews];
[self.delegateForViews clearView:centerView];
[self.delegateForViews setupView:centerView forPosition:currentPosition];
for (int i=centerIndex-1; i>=0; i--) {
UIView* prevView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:prevView];
[self.delegateForViews setupView:prevView forPosition:
[self getPosition:currentPosition-1 noOfViews:noOfViews]];
}
for (int i=centerIndex+1; i<visibleViews.count; i++) {
UIView* nextView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:nextView];
[self.delegateForViews setupView:nextView forPosition:
[self getPosition:currentPosition+1 noOfViews:noOfViews]];
}
}
}
}
}
One of the simple and that offered me everything i need is this class:
https://github.com/jakemarsh/JMStatefulTableViewController
You just need to subclass JMStatefulTableViewController and the it has 3 methods that you need to overwrite:
one that is called on init, to get the initial data
statefulTableViewControllerWillBeginInitialLoading
one when the user pull to refresh
statefulTableViewControllerWillBeginLoadingFromPullToRefresh
one when is called for the infinite scroll (next page)
statefulTableViewControllerWillBeginLoadingNextPage
This can be used from Cocoapods too.
scrollviewDidScroll will call when you move through the rows in tableview
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//check for the visible rows
let indexpath = self.tableView.indexPathsForVisibleRows?.last
//check if the visible row last is equal to the total number of counts
if(indexpath?.last == self.listCount){
//code for adding data to the tableview and reload the table view.
}
}
look in the link for more details about indexPathForVisibleRows
https://developer.apple.com/documentation/uikit/uitableview/1614885-indexpathsforvisiblerows
This is related to another question of mine which wasn't answered in a helpful way (message when a UITableView is empty).
I'm trying to show an UIImage graphic that says You haven't saved any bookmarks over an UITableView when it's empty. I have NSNotification set-up so that when bookmarks are added or deleted, a message is sent so that the UITableView can be updated.
I've been trying to do it with this code. Why won't this work?
- (void)bookmarksChanged:(NSNotification*)notification
{
[self.tableView reloadData];
UIImageView* emptyBookmarks = [[UIImageView alloc] initWithFrame:CGRectMake(75, 100, 160, 57)];
emptyBookmarks.alpha = 1;
emptyBookmarks.image = [UIImage imageNamed:#"emptyBookmark.png"];
[self.view addSubview:emptyBookmarks];
[emptyBookmarks release];
if ([self.dataModel bookmarksCount] == 0)
{
emptyBookmarks.alpha = 1;
}
else
{
emptyBookmarks.alpha = 0;
}
}
I'm probably approaching this the wrong way... But if salvageable, what am I doing wrong?
When I initially have an empty bookmarks tableview, there's no image displayed. After I add a bookmark and then delete it, the image shows. Grrh.
Another way (and IMO the correct way) to do this is to manipulate the backgroundView property on the UITableView.
While making a single cell with a custom image cell would certainly works, I think it overly complicates the logic of your UITableViewController's data source. It feels like a kludge.
According to UITableView documentation:
A table view’s background view is automatically resized to match the
size of the table view. This view is placed as a subview of the table
view behind all cells , header views, and footer views.
Assigning an opaque view to this property obscures the background color
set on the table view itself.
While you probably don't want to just set it to your UIImageView, it is very easy to make a UIView that contains the UIImageView that you want.
Well first off if you were going to do it that way, you would need to reload the tableView after updating the image or model etc. and not before.
But you are probably making things more complicated than they need to be!
Why not just check to see if the data for section 0 and indexPath.row 0 are empty and if so in cellForRowAtIndexPath display a text message accordingly.
// First make sure there is always one row returned even if the dataModel is empty.
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger numRows = 0;
if ([self.dataModel lastObject]) {
// Return the number of rows in the section.
numRows = [self.dataModel count]; // etc.
}
if (numRows < 1) numRows = 1;
return numRows;
}
// Then display the data if there is some, otherwise a message if empty.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
if ([self.dataModel lastObject]) {
// setup the cell the normal way here.
} else { // the datasource is empty - print a message
cell.textLabel.text = nil;
cell.detailTextLabel.text = NSLocalizedString(#"You haven't saved any bookmarks", #"");
cell.detailTextLabel.textColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.7];
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
Are you sure [self.dataModel bookmarksCount] is equal to 0 ?
While I agree that you are probably going about this the wrong way,
your image is allocated and added in your bookmark changed, your notification does not trigger when there are no bookmarks initially. Hence you don't see the image. Call the bookmar changed when your table view inits or appears.
Probably the best way to achieve this is to perform a check in your numberOfRowsInSection method to return 1 if your data source is empty. Then in cellForRowAtIndexPath check if your data source is empty and if it is, create a custom cell that contains whatever you want. In heightForRowAtIndexPath you need to return your custom cell height if your datasource is empty, but only if you want the cell larger than the default. At least that is how I would approach it.
when bookmarks count is nil add one to your row method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
int c;
c = bookmarks.count;
if(c == 0){
c = 1;
}
return c;
}
and then the same check again in your cellforrowatindexpath.
Another thing to be aware of in this situation is that if you're using core data and you're datasource is feeding off an entity, you will want to make sure your model matches. You can get some weird side-effect behavior in certain situations. This is especially true if you allow editing and core data has an empty model but you're tableview is still showing a cell.
When using the following code to re-size a table row the last line of text is always cutoff, no matter how many lines there are. But there is white space added that looks like enough space for the text.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
CGFloat restOfTheCellHeight = tableView.rowHeight - cell.detailTextLabel.frame.size.height;
CGSize constrainedSize = CGSizeMake(cell.detailTextLabel.frame.size.width, CGFLOAT_MAX);
CGSize textHeight = [cell.detailTextLabel.text sizeWithFont:cell.detailTextLabel.font constrainedToSize:constrainedSize lineBreakMode:cell.detailTextLabel.lineBreakMode];
CGFloat newCellHeight = (textHeight.height + restOfTheCellHeight);
if (tableView.rowHeight > newCellHeight) {
newCellHeight = tableView.rowHeight;
}
return newCellHeight;
}
Here is the code in cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomCellTableRowTypeSingleLineValueSmallLabel *cell = (CustomCellTableRowTypeSingleLineValueSmallLabel *)[tableView dequeueReusableCellWithIdentifier:#"CellTypeMultiLineLabelInCellSmallCell"];
if (cell == nil) {
NSArray *xibObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCellTableRowTypeSingleLine" owner:nil options:nil];
for(id currentObject in xibObjects) {
if([currentObject isKindOfClass:[CustomCellTableRowTypeSingleLineValueSmallLabel class]]){
cell = (CustomCellTableRowTypeSingleLineValueSmallLabel *)currentObject;
}
}
cell.accessoryType = UITableViewCellAccessoryNone;
cell.editingAccessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
cell.detailTextLabel.lineBreakMode = UILineBreakModeWordWrap;
cell.detailTextLabel.numberOfLines = 0;
cell.detailTextLabel.text = self.attributeStringValue;
cell.textLabel.text = self.rowLabel;
return cell;
}
Any ideas?
You need to call [cell.detailTextLabel sizeToFit] in order for the label to actually resize in cellForRowAtIndexPath. It will not resize on its own just because you set numberOfLines to 0. See this question and read its answers for more clarification.
You are calculating the cell height appropriately in your heightForRowAtIndexPAth method, but then in your cellForRowAtIndexPath method you are never actually using it to set the height of your label within it.
So the table is allocating the right amount of space based on your heightForRowAtIndexPath, but then inserting into that space the unresized cell that you return from cellForRowAtIndexPath. I think this might the the cause of the problem and would explain the results you are seeing.
In cellForRowAtIndexPath you need to actually set the height of the label using the same calculation.
i.e.
CGSize constrainedSize = CGSizeMake(cell.detailTextLabel.frame.size.width, CGFLOAT_MAX);
CGRect cframe = cell.detailTextLabel.frame;
cframe.size.height = constrainedSize.height;
cell.detailTextLabel.frame = cframe;
You may also need to actually set the content view frame as well (not sure how it works with a non-custom cell).
I'm also not sure its a good idea to be calling cellForRowAtIndexPath from the heightForRowAtIndexPath method (it would probably be better to just directly access the text data you are using for the size calculation directly).
Turns out I just needed to enable all of the Autosizing options in interface builder for the label.