The user first sees an image at the top of the page; the rest of the page is a UITableView directly below the image.
When the user slides up on the table, I'd like to slide the entire UITableView up so that it covers the image, and then start scrolling the table cells. Sliding down (once the first cell is at the top) the UITableView would then slide down to reveal the image again.
This is similar to what the Crackle app (and other apps) do. What is a good / elegant way to do this?
Change the position of TableView by setting its frame,
self.tableView.frame=self.view.frame in
-scrollViewWillBeginDragging:
There are undoubtedly several ways to accomplish this. On way involves having that image inside the first cell, which would be different than the other cells with your data. When you scroll, the position of that image in the cell would be moved down, which would make it appear that the image is being covered by the cell below. The code below shows how to do this (I modified this from another project that had alternating rows that appeared to float over fixed images).
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine",#"Black",#"Brown",#"Red",#"Orange",#"Yellow",#"Green",#"Blue"];
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
RDCell *topCell = (RDCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
[topCell updateImageViewWithOffset:scrollView.contentOffset.y];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.theData.count + 1;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat height = (indexPath.row == 0)? 140 : 44;
return height;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0) {
RDCell *cell = [tableView dequeueReusableCellWithIdentifier:#"ImageCell" forIndexPath:indexPath];
return cell;
}else{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.theData[indexPath.row - 1];
return cell;
}
}
The code in the custom cell was just this one method,
-(void)updateImageViewWithOffset:(CGFloat) offset {
CGFloat cellY = self.frame.origin.y;
self.topCon.constant = offset - cellY;
}
topCon is an IBOutlet to an NSLayoutConstraint, between the top of the cell and the top of the image view in the cell -- it's important the the image view have constraints to the top and sides of the cell, and a height constraint (equal to the height of the cell), but no constraint to the bottom of the cell. If you want to see if this approach gives you the look you want, you can download the sample project from here, http://jmp.sh/zOVz6Ev.
Related
Most of the time, when my app is working the way it should, my Table View items look like this:
But every so often a cell (on initial load) looks likes this:
As you can see the image has resized, the 'published By' label has resized.
Why would this happen? The same code/storyboard should affect all the cells the same way? Why are some not doing what they are told?
If it helps, when a cell loads the wrong way, all I have to do is scroll up, and back down again, and the problem is fixed !!
This means that there clearly isn't a problem with the image or the amount of text, is it just the iPhone acting up?
Thanks for any help !
I think its cell dequeue issue. Your cell could not calculate proper height for cell. If you are using autolayout try the following code. hope it will works for you.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
static YOUR_TABLEVIEW_CELL *sizingCell = nil;
static NSString *CellIdentifier=#"YOUR_TABLEVIEW_CELL_IDENTIFIER";
sizingCell =[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (sizingCell==nil)
{
sizingCell=[[YOUR_TABLEVIEW_CELL alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
[self configureFareIssueCell:sizingCell atIndexPath:indexPath];
return [self calculateHeightForConfiguredSizingCell:sizingCell];
}
//assign all the lables & images here
- (void)configureFareIssueCell:(YOUR_TABLEVIEW_CELL* )cell atIndexPath:(NSIndexPath *)indexPath
{
//e.g
cell.lbl.text=#"YOUR_TEXT";
cell.imageView.image=[UIImage imageNamed:#"NAME_OF_YOUR_IMAGE"];
}
- (CGFloat)calculateHeightForConfiguredSizingCell:(YOUR_TABLEVIEW_CELL *)sizingCell
{
CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height + 1.0f; // Add 1.0f for the cell separator height
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier=#"YOUR_TABLEVIEW_CELL_IDENTIFIER";
YOUR_TABLEVIEW_CELL *cell =[tableView dequeueReusableCellWithIdentifier:#"YOUR_TABLEVIEW_CELL_IDENTIFIER"];
if (cell==nil)
{
cell=[[YOUR_TABLEVIEW_CELL alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
[self configureFareIssueCell:cell atIndexPath:indexPath];
return cell;
}
Do you use layer mask for creating rounded image? If yes, you see this strange behavior because layer mask was created before UITableView assign proper frame for cell, so layer mask will have incorrect frame.
Here's what I'm trying to do.
I have a UITableViewCell lets say with fixed height of 300 (it is actually a variable size height but I'm trying to simplify the example)
What I want to achieve is that when I scroll back up - I will have a "thumbnailed" version of the cell - with height of 75
I managed to make it happen, but now the problem is that when I scroll up the previous cell heights are adjusted and the scroll position "jumps" once the cell sizes are smaller, which causes the view to "jump back down" when he scrolls up.
How can I adjust it?
The code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
if (indexPath.row < lastViewedChapter)
{
cell = [self generateChapterCell:tableView indexPath:indexPath collapsed:YES];
}
else
{
cell = [self generateChapterCell:tableView indexPath:indexPath collapsed:NO];
if (indexPath.row > lastViewedChapter)
{
lastViewedChapter = indexPath.row;
}
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row < lastViewedChapter)
{
return 73;
}
else
{
return 300; //actually here is a code that calculates the height
}
}
You've reduced height of the upper cell and then other cells moved up to fill that space while you were still scrolling right?
Try to set new tableView.contentOffset when you change the cell's height.
In your case the contentOffset.y should be (old contentOffset.y - (300 - 73)) when you return the cell's height as 73.
I didn't test on this but I think it may help and you must calculate new contentOffset for other case too (when scroll down, when table reload data).
static NSInteger _lastRow = -1;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (_lastRow == -1) {
_lastRow = indexPath.row;
return 300;
} else {
if (_lastRow > indexPath.row) {
_lastRow = indexPath.row;
if ([tableView rectForRowAtIndexPath:indexPath].size.height == 300) {
[tableView setContentOffset:CGPointMake(tableView.contentOffset.x, (tableView.contentOffset.y - (300 - 73)))];
}
return 73;
} else {
_lastRow = indexPath.row;
return 300;
}
}
}
This code work fine but still has some bugs (the first row height when first load data is like you have scroll up to it once, when you scroll up to top fast it bounced not normally) but I hope this should help you.
This is something that will definitely happen since you have changed cell heights.
The question is how to mitigate this kind of bad user experience.
UITableView are subclassed from UIScrollView. UIScrollViews provide delegate which is available in UITableView class as well.
Do the following.
self.tableView.delegate = self;
And then implement the following function. In the following, location is a CGPoint variable defined in your header.
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
location = tableView.contentOffset;
}
-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
CGPoint newLocation = tableView.contentOffset;
if (CGPointEqualToPoint(location, newLocation))
{
NSLog(#"are equal");
tableView.contentOffset = CGPointMake(location.x, location.y-227);
}
}
I am newbie in iOS development and i know this question is asked many times but i confuse for it. I want to know how to set a space between UITableview cell in section.In my app UITableview contain two section first section contain only one data value so not any problem. but my second section contain 5 to 7 data value But not a space between them how to set a space in footer between second section cell in UITableview.
OK so you have two methods for sections
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
and
-(UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
but neither serve the purpose of putting space between cells in a single section.
For my money there are two options... first and most complicated / hacky
in - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView you could return the count of the cells in the second section and in cellForRowAtIndexPath if (section == 0) put the data in the usual way... else you can pull the info out of the array or whatever using [theArray objectAtIndex:indexPath.section] instead.
(much simpler, and better practice).. Create a UITableViewCell subclass and use that in your cellForRowAtIndexPath like so
(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
static NSString *MyCellIdentifier = #"MyCustomCell";
switch (indexPath.section) {
case 0:
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell)
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
// setup your 1st section cells here
return cell;
}
default:
{
MYTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyCellIdentifier];
if (!cell)
cell = [[MYTableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyCellIdentifier];
// all other layout code here if indexPath.row == 0 etc.
return cell;
}
}
in your custom cell .m you can set
-(void)layoutSubviews
{
[super layoutSubviews];
self.imageView.frame = CGRectMake(0,0,0,0); // lay them out at the top / middle / wherever in your cell
self.textLabel.frame = CGRectMake(0,0,0,0); // which will allow for some space at the bottom / edges
}
then finally use
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.section == 0)
return THE_CELL_HEIGHT;
else
return THE_CELL_HEIGHT + PADDING;
}
This way you can set padding in the cell itself, which is cleaner and reusable. If you want different colours to mark the spacing, you should create UIViews in the custom UITableViewCell subclass method
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
and add them using [self.contentView addSubView:someView]; you can always set the backgroundColor of a cell to [UIColor clearColor]; if you have an image / content behind the tableView
I have a table with a custom cell, in the cells are two UIViews the top one is always displayed and the bottom one is hidden.
When the cellCellForRowAtIndexPath is called the cells are all reduced in height to just show the top UIView.
When the user clicks the cell then that cell is expanded and then I unhide the second UIView. The problem is that sometimes the second UIView does not appear on first click on the cell.
Clicking the cell again makes it all disappear and then another click and it always appears perfectly. Scroll down and then pick another and first click it may not appear or will be in wrong place, two more licks and it is perfect.
I think it is a ReusableCell issue but I have no idea how to get around it.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
cellID = #"Cell";
customCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
// Test to see if the Cell has already been built
if (cell == nil)
{
cell = [[customCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}
// Check if the selected Cell is being drawn and unhide the pull down view and adjust y origin
if(indexPath.row == selectedCell)
{
// Unhide the view
cell.pullDownView.hidden = NO;
CGRect tempFrame = cell.pullDownView.frame;
tempFrame.origin.y = [[secondViewY objectAtIndex:indexPath.row] floatValue];
cell.pullDownView.frame = tempFrame;
// Image in second view
cell.mainImage2.image = [theImages objectAtIndex:indexPath.row];
}
else
{
cell.pullDownView.hidden = YES;
}
// Image in first view
cell.mainImage.image = [theImages objectAtIndex:indexPath.row];
return cell;
}
So clicking a cell calls the didSelectRow which looks like this
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(selectedCell == indexPath.row)
{
selectedCell = -1;
[tableView deselectRowAtIndexPath:indexPath animated:NO];
}
else
{
selectedCell = indexPath.row;
}
// [tableView reloadData];
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationMiddle];
[tableView endUpdates];
}
I have tried both reloadData and BeginUpdates as you can see but this still does not work. Is this because I am hiding/unhiding should I do Alpha to 0 and then 1 when selected? Is this a reusable cell thing?
Ohhh for completeness. You may notice I have a table where all the heights are different which is why I change the Y of the second UIView.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
float returnHeightValue;
if(indexPath.row == selectedCell)
{
returnHeightValue = [[selectedCellHeights objectAtIndex:indexPath.row] floatValue];
}
else
{
returnHeightValue = [[normalCellHeights objectAtIndex:indexPath.row] floatValue];
}
return returnHeightValue;
}
Okay I have changed how I address the items. I have removed the UIView (which they were all embedded in) and instead I now have in my .h of my custom cell.
#property (nonatomic, strong) IBOutletCollection(UILabel) NSArray *pullDownItems;
#property (weak, nonatomic) IBOutlet UIImageView *mainImage;
Rather than
#property (weak, nonatomic) IBOutlet UIView *pullDownView;
Okay so now I have to unhide and move two items rather than one but that is way better than ding them individually would have been a pain (there is around 10 UILabels). I have enumerate the array like so;
for(UILabel *obj in cell.pullDownItems)
{
obj.hidden = NO;
tempFrame = obj.frame;
tempFrame.origin.y = tempFrame.origin.y - tempYOffset;
obj.frame = tempFrame;
}
Any new items I just just attach.
Okay changed it all around as hiding and unhiding content on a cell that changed did not work so well (got artifacts left in the table and sometimes things would not show up).
I am now creating two custom cells one has all the additional objects when the cell is selected the other does not.
So far seems to be much better way of doing it.
When using "Dynamic Prototypes" for specifying UITableView content on the storyboard, there is a "Row Height" property that can be set to Custom.
When instantiating cells, this custom row height is not taken into account. This makes sense, since which prototype cell I use is decided by my application code at the time when the cell is to be instantiated. To instantiate all cells when calculating layout would introduce a performance penalty, so I understand why that cannot be done.
The question then, can I somehow retrieve the height given a cell reuse identifier, e.g.
[myTableView heightForCellWithReuseIdentifier:#"MyCellPrototype"];
or something along that line? Or do I have to duplicate the explicit row heights in my application code, with the maintenance burden that follows?
Solved, with the help of #TimothyMoose:
The heights are stored in the cells themselves, which means the only way of getting the heights is to instantiate the prototypes. One way of doing this is to pre-dequeue the cells outside of the normal cell callback method. Here is my small POC, which works:
#import "ViewController.h"
#interface ViewController () {
NSDictionary* heights;
}
#end
#implementation ViewController
- (NSString*) _reusableIdentifierForIndexPath:(NSIndexPath *)indexPath
{
return [NSString stringWithFormat:#"C%d", indexPath.row];
}
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(!heights) {
NSMutableDictionary* hts = [NSMutableDictionary dictionary];
for(NSString* reusableIdentifier in [NSArray arrayWithObjects:#"C0", #"C1", #"C2", nil]) {
CGFloat height = [[tableView dequeueReusableCellWithIdentifier:reusableIdentifier] bounds].size.height;
hts[reusableIdentifier] = [NSNumber numberWithFloat:height];
}
heights = [hts copy];
}
NSString* prototype = [self _reusableIdentifierForIndexPath:indexPath];
return [heights[prototype] floatValue];
}
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 3;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString* prototype = [self _reusableIdentifierForIndexPath:indexPath];
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:prototype];
return cell;
}
#end
For static (non-data-driven) height, you can just dequeue the cell once and store the height:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSNumber *height;
if (!height) {
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"MyCustomCell"];
height = #(cell.bounds.size.height);
}
return [height floatValue];
}
For dynamic (data-driven) height, you can store a prototype cell in the view controller and add a method to the cell's class that calculates the height, taking into account the default content of the prototype instance, such as subview placement, fonts, etc.:
- (MyCustomCell *)prototypeCell
{
if (!_prototypeCell) {
_prototypeCell = [self.tableView dequeueReusableCellWithIdentifier:#"MyCustomCell"];
}
return _prototypeCell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Data for the cell, e.g. text for label
id myData = [self myDataForIndexPath:indexPath];
// Prototype knows how to calculate its height for the given data
return [self.prototypeCell myHeightForData:myData];
}
Of course, if you're using custom height, you probably have multiple cell prototypes, so you'd store them in a dictionary or something.
As far as I can tell, the table view doesn't attempt to reuse the prototype, presumably because it was dequeued outside of cellForRowAtIndexPath:. This approach has worked very well for us because it allows the designer to modify cells layouts in the storyboard without requiring any code changes.
Edit: clarified the meaning of sample code and added an example for the case of static height.
I created a category for UITableView some time ago that may come helpful for this. It stores 'prototype' cells using asociated objects for reusing the prototypes and provides a convenience method for obtaining the height of the row assigned in storyboard. The prototypes are released when the table view is deallocated.
UITableView+PrototypeCells.h
#import <UIKit/UIKit.h>
#interface UITableView (PrototypeCells)
- (CGFloat)heightForRowWithReuseIdentifier:(NSString*)reuseIdentifier;
- (UITableViewCell*)prototypeCellWithReuseIdentifier:(NSString*)reuseIdentifier;
#end
UITableView+PrototypeCells.m
#import "UITableView+PrototypeCells.h"
#import <objc/runtime.h>
static char const * const key = "prototypeCells";
#implementation UITableView (PrototypeCells)
- (void)setPrototypeCells:(NSMutableDictionary *)prototypeCells {
objc_setAssociatedObject(self, key, prototypeCells, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSMutableDictionary *)prototypeCells {
return objc_getAssociatedObject(self, key);
}
- (CGFloat)heightForRowWithReuseIdentifier:(NSString*)reuseIdentifier {
return [self prototypeCellWithReuseIdentifier:reuseIdentifier].frame.size.height;
}
- (UITableViewCell*)prototypeCellWithReuseIdentifier:(NSString*)reuseIdentifier {
if (self.prototypeCells == nil) {
self.prototypeCells = [[NSMutableDictionary alloc] init];
}
UITableViewCell* cell = self.prototypeCells[reuseIdentifier];
if (cell == nil) {
cell = [self dequeueReusableCellWithIdentifier:reuseIdentifier];
self.prototypeCells[reuseIdentifier] = cell;
}
return cell;
}
#end
Usage
Obtaining the static height set in storyboard is as simple as this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [tableView heightForRowWithReuseIdentifier:#"cellIdentifier"];
}
Assuming a multi-section table view:
enum {
kFirstSection = 0,
kSecondSection
};
static NSString* const kFirstSectionRowId = #"section1Id";
static NSString* const kSecondSectionRowId = #"section2Id";
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat height = tableView.rowHeight; // Default UITableView row height
switch (indexPath.section) {
case kFirstSection:
height = [tableView heightForRowWithReuseIdentifier:kFirstSectionRowId];
break;
case kSecondSection:
height = [tableView heightForRowWithReuseIdentifier:kSecondSectionRowId];
}
return height;
}
And finally if the row height is dynamic:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
id thisRowData = self.allData[indexPath.row]; // Obtain the data for this row
// Obtain the prototype cell
MyTableViewCell* cell = (MyTableViewCell*)[self prototypeCellWithReuseIdentifier:#"cellIdentifier"];
// Ask the prototype cell for its own height when showing the specified data
return [cell heightForData:thisRowData];
}