iPhone - Create custom UITableViewCell top and bottom borders - iphone

I have been looking everywhere and have not quite found my answer.
I populating a UITableView with dynamic cells from JSON and I am trying to hide any extra cells. I turned off the separators in IB, and of course all the cell separators disappear. How do I add a line to the bottom and top of each tableviewcell so that only the cells that have information show a border? I have imported Quartz and have been playing with CALayer but can't find a solution.
I found a similar question here, but the only answer was not very helpful.
What would be a better, different way of doing this?
Here are my cellForRowAtIndexPath and my numberOfRowsInSection:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
//set equal to the information in the array
return [_jsonDataArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
//create Dictionary of data in row
NSDictionary *jsoninfo = [_jsonDataArray objectAtIndex:indexPath.row];
//get required keys from dictionary and assign to vairables
NSString *title = [jsoninfo objectForKey:#"title"];
NSString *subtitle = [jsoninfo objectForKey:#"subtitle"];
NSURL *imageURL = [NSURL URLWithString:[jsoninfo objectForKey:#"series_image_URL"]];
//download the images.
NSData *imgData = [NSData dataWithContentsOfURL:imageURL];
UIImage *img = [[UIImage alloc] initWithData:imgData];
//set boarder for custom cells... I need to have a border on the top and bottom of the cells I am creating so xcode does not autofill the empty space.
//fill in text to cells
cell.textLabel.text = title;
cell.detailTextLabel.text = subtitle;
cell.imageView.image = img;
return cell;
}

I also think it's not the best idea, but if you really want to do this, here's code that will achieve what you want:
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
// Draw top border only on first cell
if (indexPath.row == 0) {
UIView *topLineView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 1)];
topLineView.backgroundColor = [UIColor grayColor];
[cell.contentView addSubview:topLineView];
}
UIView *bottomLineView = [[UIView alloc] initWithFrame:CGRectMake(0, cell.bounds.size.height, self.view.bounds.size.width, 1)];
bottomLineView.backgroundColor = [UIColor grayColor];
[cell.contentView addSubview:bottomLineView];
Put this code in the tableView:cellForRowAtIndexPath: method. The final look of your UITableView will be like this:
Take into account that this is not very good for performance, especially if you have a lot of cells. If you have a bigger amount of data, refer to this SO question for help on how to optimize the drawing.

Only try this at tableView:cellForRowAtIndexPath: method
[cell.contentView.layer setBorderColor:[UIColor grayColor].CGColor];
[cell.contentView.layer setBorderWidth:1.0f];

It sounds like a sub-optimal solution to try to "distinguish" your valid cells from empty ones with lines. A superior approach would be to clean up the data source before populating the table with it.

This is not the answer to the question, but the clean solution for the original problem.
Add an empty UIView as the footer of the UITableView. Then the empty cells will be hidden. See this answer.

Use Custom Cells. Your Datasoure (Models) should drive the information into the Custom Cells. Create a setter within the Custom Cell class that can be set at each row. as in....
Allocation your Custom Cell with reuse Id,
Pass the property that is determing if the line should show:
[cell setCustomLines:Model.property];
return the cell;
You will have far more flexibility to design the CustomCell any way you want, Images, Lines, Colors, or other ways of letting your user's see a difference among your cells.
Technically, Marco's Answer will work, and good job on a simple solution. But you will not be able to expand this very much farther this this.

Related

Events inside UITableViewCell not working below y value

I'm having some trouble getting my buttons to work below a certain y value in my table cells. I'm using a custom UITableViewCell class named "RowWhiskyContent". The default height is 44px and it's below that point my events don't seem to trigger anymore. The button displays just fine and so does everything else below that point, the event however don't seem to trigger. If i place my button half way (like at y=35) only the top part of the button triggers the event and the bottom part doesn't do a thing.
Here's the code trimmed down to the esentials:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
if(![self createView:&cell])
{
UIImage *bottle = [UIImage imageNamed:#"icon_add.png"]; //image size: 22x22
UIButton *bottleButton = [[UIButton alloc] initWithFrame:CGRectMake(60, 70, bottle.size.width, bottle.size.height)];
[bottleButton setImage:bottle forState:UIControlStateNormal];
[cell.contentView addSubview:bottleButton];
[bottleButton addTarget:self action:#selector(addToCollection:) forControlEvents:UIControlEventTouchUpInside];
cell.contentView.frame = CGRectMake(cell.contentView.frame.origin.x, cell.contentView.frame.origin.y, cell.contentView.frame.size.width, 160);
//cell.frame = cell.contentView.frame; // Tried this, didn't work.
//[tableView reloadData]; // Tried this too, didn't work either.
}
return cell;
}
// Check if cell exists and create the cell if it doesn't.
-(BOOL) createView: (UITableViewCell**) cell
{
BOOL cellExists = YES;
*cell = (RowWhiskyContent *) [myTableView dequeueReusableCellWithIdentifier:#"ContentIdentifier"];
if(*cell == nil)
{
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"RowWhiskyContent" owner:self options:nil];
*cell = [topLevelObjects objectAtIndex:0];
cellExists = NO;
}
return cellExists;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
{
return 160;
}
Since I'm setting the height of the cell and the contentView both at 160 I'm not sure what's going wrong here. Reloading the data didn't work and neither did setting the cell.frame.
Could anybody please tell me what I'm doing wrong?
Thanks in advance.
EDIT:
Added a screenshot:
Red button works fine but if I place it at the position of the green button it stops working. The contentview's background is set to purple so that explains the purple area. When clicking the cell it triggers the didSelectRowAtIndexPath so I'm guessing the the cell itself is also big enough.
This is definitely a content size issue. Your button will display outside of the frame similar to overflow in CSS, however they will not respond to events. So whatever UIView is containing your UIButton you need to make sure that it's content/frame/bounds are all set tall enough. You can also use [cell.contentView sizeToFit] to adjust it automatically to it's content.
You should definitely NOT reload data inside of the protocol methods for your UITableView.

Trying to calculate frame size of UILabel based on the amount of text for subview of cell

The code below has code that determines the frame size of a UILabel, and I think it does work, however when I place it within my rowAtIndexPath for a UItable I get wonky results.
Perhaps, I dont fully understand how or what the reuseIdentifier does, but I placed the code to calculate the frame only when the cell is nil. What happens is that the heights are calculated only for the first three cells, then it repeats in sequence for the rest of the cells. For example, cell one's height is used for cell four's height.
Maybe someone can point me in the right direction as to how I should setup my calculations.
Thanks!
if(cell == nil){
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:DisclosureButtonCellIdentifier] autorelease];
//start adding custom subviews to the table cell
//addSubview for Description
UILabel *descValue = [[UILabel alloc] init];
NSString *descString = rowData.summary;
CGSize maximumSize = CGSizeMake(185, 130);
UIFont *descFont = [UIFont fontWithName:#"HelveticaNeue" size:12];
CGSize descStringSize = [descString sizeWithFont:descFont
constrainedToSize:maximumSize
lineBreakMode:descValue.lineBreakMode];
CGRect descFrame = CGRectMake(125, 60, 185, descStringSize.height);
descValue.frame = descFrame;
descValue.backgroundColor = [UIColor redColor];
descValue.font = descFont;
descValue.tag = kDescriptionValueTag;
descValue.lineBreakMode = UILineBreakModeWordWrap;
descValue.numberOfLines = 0;
[cell.contentView addSubview:descValue];
[descValue release];
}
UILabel *desc = (UILabel *)[cell.contentView viewWithTag:kDescriptionValueTag];
desc.text = rowData.summary;
Using NSString UIKit Additions, you can get the width of a string using a particular font:
[myString sizeWithFont:myFont];
There are several other functions in that Additions set that can help figure out how big a piece of text will be, with different layout options, for single and multi-line text, etc.
The purpose of the reuseIdentifier is to let you reuse a cell -- with particular subviews in particular places -- without the device having to spend the execution time to do all that layout. Definitely more useful back in the iPhone 1 days when the processor was much slower. Until you are more familiar with reuseIdentifiers, I would suggest you just create a new cell every time that function is called.
Each time the OS calls your cellForRowAtIndexPath, you need to fill out the content correctly. Anything that needs to get resized or changed depending on the row should be set.
Frankly, I did not try understanding your code completely. That is nearly impossible especially because you close the method with brackets without returning anything but cellForRowAtIndexPath which I assume you are referring to, requires the return of a UITableViewCell object.
Apparently we are looking only at some fraction of the code.
However, layouting a cell properly is not the full task. You need to tell the table the height of each cell.
You may want to implement
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
for that task. That method is good if the cell height varies.
If the cell hight is different from the standard but does not vary from cell to cell then setting of the rowHeight property of the tableView will be sufficient.
I think that in your case the implementation of the method heightForRowAtIndexPath is mandatory.
BTW, you may want to look at subclassing the UITableCellView and implement the layoutSubviews method in your subclass.
You didn't show all of your code but usually to implement tableView:cellForRowAtIndexPath: you start with the following:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// get a cell from the queue of reusable cells, if any
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: DisclosureButtonCellIdentifier];
if (cell == nil) {
// No cell to reuse, need to create a new one
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:DisclosureButtonCellIdentifier] autorelease];
// add cell subviews here
}
// fill in cell content and resize subviews here
...
return cell;
}
Once you have created 3 (in your case) cells they get reused as the table scrolls so you don't have to keep creating cells. The call to dequeueReusableCellWithIdentifier will return one of the previously created cells if it was not longer in use (it scrolled off the top or the bottom).

"reset" cells after changing orientation

I added the interfaceOrientation to my app. It works fine concerning the views. Some of the table-cells I defined by CGRects to position the text in the cell. In portrait-mode the cell is 300px long, in landscape-mode 420px. I use the following code to change the CGRects depending the orientation:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.interfaceOrientation == UIDeviceOrientationPortrait) {
NSString *currentLanguage = [[NSString alloc] initWithContentsOfFile:[NSHomeDirectory() stringByAppendingPathComponent:#"/Documents/sprache.txt"]];
static NSString *TableViewTableCellIdentifier = #"TableViewTableCellIdentifier";
UITableViewCell *cell = (UITableViewCell *) [tableView dequeueReusableCellWithIdentifier:TableViewTableCellIdentifier];
CGRect cellRect = CGRectMake(0, 0, 300, 175);
cell.backgroundColor = [UIColor darkGrayColor];
cell = [[[UITableViewCell alloc] initWithFrame:cellRect reuseIdentifier:TableViewTableCellIdentifier] autorelease];
CGRect keyLabelRect = CGRectMake(0, 5, 5, 20);
UILabel *keyLabel = [[UILabel alloc]initWithFrame:keyLabelRect];
keyLabel.tag = 100; //.........
} else {
NSString *currentLanguage =
[[NSString alloc] initWithContentsOfFile:[NSHomeDirectory() stringByAppendingPathComponent:#"/Documents/sprache.txt"]];
static NSString *TableViewTableCellIdentifier = #"TableViewTableCellIdentifier";
UITableViewCell *cell = (UITableViewCell *) [tableView dequeueReusableCellWithIdentifier:TableViewTableCellIdentifier];
CGRect cellRect = CGRectMake(0, 0, 450, 175);
cell.backgroundColor = [UIColor darkGrayColor];
cell = [[[UITableViewCell alloc] initWithFrame:cellRect reuseIdentifier:TableViewTableCellIdentifier] autorelease];
CGRect keyLabelRect = CGRectMake(0, 5, 5, 20);
UILabel *keyLabel = [[UILabel alloc] //.....
}
}
My problem is, when the table is visible and the orientation is changed, I need to scroll to see the new "layout". How can I manage to "reload" the view after changing the orientation?
First of all, this will leak like mad. You create cells and dequeue them but you never release them. You just create an entirely new cell every time a cell is requested. There is no reason to recreate a cell if you dequeue and vice versa. More importantly, your creating as many cells as you have rows in your logical table. That will eat all your memory very quickly.
When you dequeue a cell, you need to check if it's frame is the right size and reuse it if it is. If it is not, then you need to release the cell and create another one.
Second, a table will not ask for new cells until you scroll the existing cells off the screen. This is why your cells do not change until you scroll. It's the expected behavior of a tableview.
My standing recommendation for any moderately complex view is to use different view-controller/view pairs for each orientation. It seems like more work but usually I find it it actually takes less and it's easier to manage. In this case, having two separate tables will probably save you a lot of grief.
I'm pretty sure that you do not need to specify frame during default UITableViewCell construction. Usually frame size is handled by UITableView itself.
But if you wish you can send setNeedsLayout and/or setNeedsDisplay message to a tableView to force it update cell layout (in a orientation handler).

How to increase the speed of the scroll in the table view when images are being loaded in each cell?

Check out the last lines before [return cell]..After the images are being loaded the scrolling speed is decreasing..it seems the scroll gets stuck
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
}
int storyIndex = [indexPath indexAtPosition: [indexPath length] - 1];
NSString *itemDescription=[[stories objectAtIndex: storyIndex]
objectForKey:#"title"];
CGRect aframe = CGRectMake(80, 30, 250, 40);
textLabel = [[UILabel alloc] initWithFrame:aframe];
textLabel.font = [UIFont boldSystemFontOfSize:14];
textLabel.numberOfLines=0;
textLabel.textColor = [UIColor darkTextColor];
textLabel.backgroundColor = [UIColor whiteColor];
[cell.contentView addSubview:textLabel];
textLabel.text=itemDescription;
CGRect frame = CGRectMake(0, 0, 70,80);
UIImageView *TopImageView = [[UIImageView alloc] init];
[cell.contentView addSubview:TopImageView];
TopImageView.frame=frame;
m_strImage = [m_imglinkArray objectAtIndex:storyIndex];
TopImage = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:m_strImage]]];
TopImageView.image=TopImage;
return cell;
}
Could you guys help me to increase the speed of the scroll?
Regards
Arun
I believe you may have two problems here. First is the issue of not multi-threading the image loads and second is how you're using UITableViewCell.
iPhone calls cellForRowAtIndexPath whenever a cell appears on the screen - if you scroll down, everything above is unloaded and reloaded when you scroll back up again. As already noted this can be solved with multithreading patterns - either the link posted to markj.net, and/or by using NSThread. The advantage of NSThread is you have more flexibility to handle problems or load more than just image data. (I would use both)
In order to do this efficiently you need to have a custom UITableViewCell. This is good to have regardless because you can make the table cell responsible for it's content rather than your view controller, and you can efficiently manipulate cell formatting and content. Here is a link to a great post about UITableCellView: http://blog.atrexis.com/index.cfm/2009/1/6/iPhone--Customize-the-UITableCellView
When you implement UITableViewCell, do your best to put all addSubView and formatting calls in the "if(cell==nil)" block, only if you can't put them in your custom initWithFrame. You'll find the iPhone is stacking every subview you add -- it's not clearing the old view. You can see this at work by setting all the background colors to "UIColor clearColor" and then change text between hits -- you'll see a stack of text on top of each other. This isn't normally visible because solid-filled backgrounds draw over all the "old" subviews.
Before you combine these two methods, considering implementing a "model". You've got a view and a controller but all of your data should be in a model. All of the image URLs, content, etc. should be in something like an NSMutableArray, a custom object of your own, or maybe via SQLite. Once you have a model, you can implement your caching of the images. With caching, all of the images will be retained between loads of your application which will save batteries, bandwidth, and time.
I would:
1. put all your data in a model of some kind (NSMutableArray, SQLLite, something)
2. Implement your own custom UITableViewCell
3. Make sure you have 0 addSubView, init, or alloc calls inside the active block of cellForRowAtIndexPath
4. Use delegation example or NSThread to load all images in the background
5. Add local image caching
There are some good examples out in about and on the Apple forums on caching images.
nessence has a lot of good information. A few more thoughts:
You're leaking like crazy here. Every cell you create, you leak 2 UILabels, a UIImageView and a UIImage.
As noted before, not only are you leaking these, they're accumulating in your view because you're sticking one on top of the other with addSubview:.
Reaching out to the network during a cell draw is incredibly slow and is blocking your UI. If these URLs are local, then you can use UIImage's +imageWithContentsOfFile, if not, you need to load this in the background.
I don't think you need a thread here. NSURLConnection is an excellent way to load data in the background without incurring the overhead of threading.
nessence is completely correct that you need a model class for Story.
Your basic approach to reusable cell configuration is incorrect. You don't fetch a reusable cell and then add subviews to it. All your subviews should be added in the if() block to create the cell. Then, in each pass, you just change the values of things. I've rewritten some of your code below to demonstrate. This is still not correct code, because it is reaching out to the network during a cell draw, and this may be too many elements to be in a subview-cell (rather than a custom cell), but it's closer to the right idea. I don't even know if this compiles; I just typed it here.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
// Here we do all our cell creation
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
// Make the label
CGRect aframe = CGRectMake(80, 30, 250, 40);
UILabel *textLabel = [[[UILabel alloc] initWithFrame:aframe] autorelease]; // Note the -autorelease so we don't leak
textLabel.font = [UIFont boldSystemFontOfSize:14];
textLabel.numberOfLines=0;
textLabel.textColor = [UIColor darkTextColor];
textLabel.backgroundColor = [UIColor whiteColor];
textLabel.tag = DescriptionTag; // A tag so we can find it later (you'll need a constant for this)
[cell.contentView addSubview:textLabel];
// And the second label
aframe = CGRectMake(80, 30, 250, 40);
textLabel = [[[UILabel alloc] initWithFrame:aframe] autorelease];
textLabel.font = [UIFont boldSystemFontOfSize:14];
textLabel.numberOfLines=0;
textLabel.textColor = [UIColor darkTextColor];
textLabel.backgroundColor = [UIColor whiteColor];
textLabel.tag = TitleTag;
[cell.contentView addSubview:textLabel];
// The image view
CGRect frame = CGRectMake(0, 0, 70,80);
UIImageView *topImageView = [[[UIImageView alloc] init] autorelease];
topImageView.frame = frame;
topImageView.tag = TopImageTag;
[cell.contentView addSubview:topImageView];
}
// all the above was cell creation; we do that as seldom as possible.
// Now we do cell configuration. We want this to be fast.
UILabel *descriptionLabel = (UILabel*)[cell.contentView viewWithTag:DescriptionTag];
descriptionLabel.text = itemDescription;
UILabel *titleLabel = (UILabel*)[cell.contentView viewWithTag:TitleTag];
titleLabel.text =[[stories objectAtIndex:indexPath.row] objectForKey:#"title"];
NSString *imageURLString = [m_imglinkArray objectAtIndex:storyIndex]; // You should have a model class called Story, not two arrays.
UIImage *image = [[[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURLString]]] autorelease]; // This is still way too slow if it's a remote URL
UIImageView *imageView = (UIImageView*)[cell.contentView viewWithTag:TopImageTag];
imageView.image = image;
return cell;
}
I recommend you spend some quality time studying TableViewSuite, Practical Memory Management, and Coding Guidelines for Cocoa. Some time studying the basics of Cocoa would be useful as well, because the coding style here indicates that you may not have a solid foundation. Though it's a Mac book, I still recommend Cocoa Programming for Mac OS X. If you're interested in using this to learn iPhone, I've put together a syllabus that might help. I haven't reviewed it yet, but Stanford's online CS193P course looks promising.
this has been asked several times here, what you need to do is to load asynchronously the images for each cell in order to prevent the scroll to slow down.
This is called "Lazy image loading" and I'm referencing the apple tutorial here :
Lazy load images in UITableView
It seems that you are loading from a url every time the table view asks to get the cell for a row. This is happening in the main application thread (the User interface thread) and it blocks the user interface, making it not responsive.
Especially if this url is a resource that you load from the internet then this pause is big.
I would recommend that you should load the images in a background thread and display a placeholder image until they are fetched from the network.
You can try the solution provided in the following url...
http://www.markj.net/iphone-asynchronous-table-image/

Layout options in UITableView (iPhone)? Or extend UIScrollView?

I'm new to Objective-C and iPhone coding and was hoping someone could help me.
Basically I want a scrollable table that displays a name and a quantity of that item. For example:
Apples.........2
Oranges......4
Bananas......5
I want the name to be left-justified and the number to be right-justified.
Is there any way to do this with a UITableView or will I need to extend UIScrollView?
Thanks in advance.
Set cell.text to "Apples". Then create an new UILabel view with the text "2" and assigned it to cell.accessoryView.
Text is always on the left, and accessory views are always on the right. Optionally a UIImage in cell.image will be displayed to the left of the cell text if you choose to add one.
If you have a cell layout more complicated than that you have to add subviews to your cell manually.
One not too difficult option is to subclass UITableViewCell with one label for the text and one label for the number, then just set the label values.
If you want the ... that you've shown then you could probably use a standard UITableViewCell and write some code to feed the cell text that has been pre-modified to fit in the space alloted.
You could simply use another CellType, which is already provided by apple and change the color for the detailTextLabel:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CustomCellIdentifier = #"Cell";
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CustomCellIdentifier];
cell.textLabel.text = #"Apples";
cell.detailTextLabel.text = #"2";
cell.detailTextLabel.textColor = [UIColor blackColor];
return cell;
}
No need to bother with subclassing or adding labels on your own.