I'm currently writing an application that's meant to scale with data brought in from the back-end. To test this scalability, I'm currently using plists to bring in some basic data.
The plan is to eventually have male/female icons appear next to the user names for gender/visual purposes (using colors currently), however, after writing a conditional statement in the proper tableView method, the application seems to skip those arguments entirely.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SelectProfileCell *cell = (SelectProfileCell *) [tableView dequeueReusableCellWithIdentifier:#"SelectProfileCell"];
NSArray *array = [[NSArray alloc] initWithArray:[userArray objectAtIndex:indexPath.row]];
cell.nameLabel.text = [array objectAtIndex:0];
gender = [array objectAtIndex:1];
if(gender == #"Male"){
cell.genderLogo.backgroundColor = [UIColor blueColor];
NSLog(#"Male");
}
if(gender == #"Female"){
cell.genderLogo.backgroundColor = [UIColor greenColor];
NSLog(#"Female");
}
return cell;
}
In this instance, both userArray and gender are instance variables that have already been initialized. "userArray" has already been passed the appropriate 2D Array data from the property list (used the console to check this).
Again, the log is not showing the "Male"/"Female" debug messages, so these "if" statements are being skipped for some reason. Any suggestions?
Thank you!
use [gender isEqualToString:#"Male"] instead of ==
Related
My UITableView, after the messages (content) is loaded into the cells, experiences a very noticeable lag in scrolling and sometimes freezes up for a few seconds. This is weird because all the messages are loaded once the user scrolls. Any ideas on how to make this fast scrolling no problem?
Thank you!
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *simpleTableIdentifier = #"MailCell";
MailCell *cell = (MailCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MailCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
// Anything that should be the same on EACH cell should be here.
UIView *myBackView = [[UIView alloc] initWithFrame:cell.frame];
myBackView.backgroundColor = [UIColor colorWithRed:40.0/255.0 green:148.0/255.0 blue:196.0/255.0 alpha:1];
cell.selectedBackgroundView = myBackView;
cell.messageText.textAlignment = NSTextAlignmentLeft;
cell.messageText.lineBreakMode = NSLineBreakByTruncatingTail;
}
NSUInteger row = [indexPath row];
// Extract Data
// Use the message object instead of the multiple arrays.
CTCoreMessage *message = [[self allMessages] objectAtIndex:row];
// Sender
CTCoreAddress *sender = [message sender];
NSString *senderName = [sender name];
// Subject
NSString *subject = [message subject];
if ([subject length] == 0)
{
subject = #"(No Subject)";
}
// Body
BOOL isPlain = YES;
NSString *body = [message bodyPreferringPlainText:&isPlain];
body = [[body componentsSeparatedByCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]]
componentsJoinedByString:#" "];
body = [body stringByReplacingOccurrencesOfString:#" " withString:#" "];
// Populate Cell
[[cell nameText] setText:senderName];
[[cell subjectField] setText:subject];
[[cell messageText] setText:body];
if ([message isUnread])
{
cell.nameText.textColor = [UIColor colorWithRed:15.0/255.0 green:140.0/255.0 blue:198.0/255.0 alpha:1];
}
else
{
cell.nameText.textColor = [UIColor blackColor];
}
return cell;
}
xCode comes with a profiler called Instruments. It's CPU time profiler is perfect for figuring out which code is slowing things down. Run your app with the profiler and spend a few seconds just scrolling around. It will give you statistics.
Keep in mind, the code inside if (cell == nil) will run about 10 times (UITableView caches just enough cells to fill itself). But the code outside the if is expensive - it runs every time a cell becomes visible.
I would guess the most expensive operations in the code you posted are:
Giving iOS too many subviews to draw on a cell
Do your own drawing instead.
Replacing runs of whitespace in the entire body text with single spaces
The code you posted allocates new strings for each word, plus an array to hold them. Then it allocates two more copies (one with words rejoined and one with runs of spaces compacted). It processes the entire body text string, even if the majority will never be visible to the user in a tiny preview of the body!
Cache the resulting string so that this operation is performed only once per cell.
Also, you can create a new mutable string, reserve space in it, and copy characters from the original in a loop (except runs of whitespace). Instead of processing the entire body text, you could stop at 100 characters or so (enough to fill a table cell). Faster and saves memory.
Slow UITableView scrolling is a very very common question. See:
How to solve slow scrolling in UITableView
iPhone UITableView stutters with custom cells. How can I get it to scroll smoothly?
Nothing seems wrong with your code. I'd recommend using a table optimization framework such as the free Sensible TableView.
I have an iPhone app which has a Table View-based data input screen with a toggle, which when on shows all rows in another section of the table.
Sometimes, when the app is first loaded, and usually when it has been fully deleted from the phone, the UITextFields from the original section of the table are displayed in addition to the new rows, as below (main table section is above)
:
THe strangest thing about this is that this behaviour only occurs the first time this screen is displayed - it seems fine after that. Oh, and it only seems to occur on the phone, not the simulator.
Given the random nature of this, could it have something to do with other apps? I did have apps with similar namespaces running at the same time. The problem seemed to go away after I closed the apps down / deleted from phone.
I have included the code block that is run when the switch is changed below:
- (void)accountSwitchChanged {
[customerDetails saveField:#"addToAccount" WithBool:addToAccountSwitch.on];
NSArray *indexes = [NSArray arrayWithObjects:[NSIndexPath indexPathForRow:1 inSection:1], [NSIndexPath indexPathForRow:2 inSection:1], [NSIndexPath indexPathForRow:3 inSection:1],nil];
if (addToAccountSwitch.on)
[detailsTable insertRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationFade];
else
[detailsTable deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationFade];
}
Any ideas?
--EDIT
cellForRowAtIndexPath code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// get rid of grey background for iPad app
[tableView setBackgroundView:nil];
UITableViewCell *cell = nil;
NSDictionary *cellData = [[dataSourceArray objectAtIndex: indexPath.section] objectAtIndex:indexPath.row];
id cellControl = [cellData objectForKey:kCellControlKey];
static NSString *kCellControl = #"CellControl";
cell = [tableView dequeueReusableCellWithIdentifier:kCellControl];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellControl] autorelease];
cell.textLabel.textColor = [[[ConfigManager sharedInstance].skin valueForKey:#"tableCellHeadingTextColour"] toUIColor];
cell.backgroundColor = [[[ConfigManager sharedInstance].skin valueForKey:#"tableCellBgColour"] toUIColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:17];
} else {
// a cell is being recycled, remove the old control (if it contains one of our tagged edit fields)
UIView *viewToCheck = nil;
viewToCheck = [cell.contentView viewWithTag:kDetailsViewTag];
if (!viewToCheck)
[viewToCheck removeFromSuperview];
}
// if control is not a text field, make it a button
if (![cellControl isKindOfClass:[UITextField class]])
cell.selectionStyle = UITableViewCellSelectionStyleGray;
else
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.textLabel.text = [cellData objectForKey:kCellTitleKey];
[cell.contentView addSubview:cellControl];
return cell;
}
Something to check and eliminate If it is only occurring the first time view is displayed, it's always worth checking whether you are assuming the view is loaded before you are doing work on it. This doesn't come up very often, but when it does it can be very hard to track down. To check whether this might be your issue, add a [viewController view] call just after you create the view for the first time. This forces the view to be loaded. If the issue goes away when you add this, you've tracked the source of the issue. And you can even leave the [viewController view] call in as a fix, or work through your code to allow for lazy instantiation.
All this said, far more likely to be something funny in your tableView:cellForRowAtIndexPath: code. If you want good stack overflow, post all or relevant fragment of the that code (or the relevant code it calls).
Follow up on posted code:
(1) if (!viewToCheck) [viewToCheck removeFromSuperview] won't do anything. It'll only send a removeFromSuperview message when viewToCheck is nil and that won't do anything. Not sure if this will be key issue, but it's something to put right.
(2) [cell.contentView addSubview:cellControl]; - this looks problematic - it's usually best to only add subviews when you create the cell. You can hide or show in the main body of tableView:cellForRowAtIndexPath: - that would, I suspect, work for you here. With this code there's a risk here that you'll either add multiple subviews when you only want one or (which may be what's going on here) that you end up not removing a subview when a cell is recycled. If the viewToCheck code is supposed to be removing the added cellControl then, as (1), it won't do that just now because your if condition is wrong.
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.
I wonder if anyone can speculate or better yet provide a piece of code as for the implementation of the lengthy friends list in the Facebook iPhone app.
when you open the app and go strait to the friends list, you get the list almost in an instant, at least for me with ~500 friends.
when I try it in my own app it takes lots of precious seconds to populate the table view with the same data, so how does Facebook accomplished such a quick response time ?
upon looking at the tableview in the facebook app you notice there is no scroll bar usually found in such tableview, could that be one sign of the neat trick facebook is utilizing to achieve this rapid rows insert ? could it be they implemented some sort of a virtual tableview with only holds a few dozen rows but rotates them ?
any thoughts ?
the UITableView will let you do this. There are a number of examples on the internet with UITableView and Custom Cell's
Essentially, you load your images in the background, and you reuse the Cells that are in the tableview
EDIT Added example code to demonstrate how this is accomplished.
IMPORTANT NOTE
This code was not tested and may or may not actually function as is.
It was pasted with some editing for length. I did a lot more then this in my app, but in the interest of keeping with the example requested I omitted a lot.
On with the example:
Here is where I get the cell, load it with the items that are readily available. And send it to the background thread to load the rest.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"OfferCell";
static NSString *CellNib = #"OfferItem";
OfferCell* cell = (OfferCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:CellNib owner:self options:nil];
cell = (OfferCell*)[nib objectAtIndex:0];
}
NSDictionary* couponPackage = [self.jsonOfferData valueForKey:#"result"];
NSArray *couponList = [couponPackage valueForKey:#"offers"];
if ([couponList count] >= indexPath.row )
{
NSDictionary* couponData = [couponList objectAtIndex:indexPath.row];
Coupon *coupon = [[Coupon alloc] initWithDictionary:couponData];
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:cell,#"cell",coupon,#"coupon", nil];
//Right here you would try to load any cached imaged from disk.
//Then send a Thread to the background to load the image.
[self performSelectorInBackground:#selector(loadTableViewCellData:) withObject:params];
//Load up the rest of the custom info into the custom cell.
[cell.captionLabel setText:coupon.name];
[cell.subTextLabel setText:coupon.subText];
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
[cell setCommand:coupon.command];
[cell setParameter:coupon.commandArgs];
[cell setImageURL:coupon.imageURL];
[cell setImageAltURL:coupon.imageAltURL];
[cell setRegistrationCode:coupon.registrationCode];
[coupon release];
}
return cell;
}
as you can see, i call a background thread before i even load the custom content in the cell.
- (void) loadTableViewCellData:(NSDictionary*) objectData
{
OfferCell *cell = [objectData objectForKey:#"cell"];
Coupon *coupon = [objectData objectForKey:#"coupon"];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[coupon iconURL]]]];
[objectData setValue:image forKey:#"image"];
self performSelectorOnMainThread:#selector(setImageOnMainThread:) withObject:objectData
}
after downloading the image, i send a Main thread request to update the Image that is in the cell object.
- (void) setImageOnMainThread:(NSDictionary*) objectData
{
OfferCell *cell = [objectData objectForKey:#"cell"];
Coupon *coupon = [objectData objectForKey:#"coupon"];
UIImage *image = [objectData objectForKey:#"image"];
cell.icon.image = image;
}
##AGAIN This May not Actually Function. ##
I did not copy all of my code for this. this is a hammer out so you can get the idea.
play with the code and test it. but the fundamentals are.
Dequeue the cell that will fit your needs (Reuse Identifier)
Use the cell if it can be dequeue'd or create a new one with a reuse identifier (my example uses a xib file named OfferItem.xib)
Send a thread to the background that will load the image data from disk or url (a combination of both is recommended)
Send a thread back to the UI when you are ready to load the image into the View (Updating the UI must be done on the main thread)
if you can do that, then your friends list (or in this case offers) will be loaded up as fast as possible. and the Images will pop on the screen as soon as they download.
Also if you use a Caching technique it will be faster for subsequent loads because in the the first method {tableView:cellForRowAtIndexPath:} you would load up the cached image immediately.
Aside from that, this should load your cell's pretty fast.
They obviously load the data from a local resource (plist, ManagedObject, ...)
Have a look at some sample code to draw a TableView:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *kCellIdentifier = #"MyCellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:kCellIdentifier] autorelease];
}
return cell;
}
The dequeueReusableCellWithIdentifier: thing is one reason why TableViews in iOS can draw quickly. It works somehow like this:
1)You provide an identifier for a cell you're creating.
2)Cells that are visible at first get alloced (with identifier)
3)When a Cell is moved off the screen it gets put on a pile MyCellIdentifier
4)Whenever the system needs to draw a cell of identifier:MyCellIdentifier it first looks whether there are any cells currently unused on the MyCellIdentifier pile. If that's the case it picks one off the pile and thus doesn't have to alloc a new one. That way expensive allocing can be kept at a minimum.
I hope this answers your question :)
I'm loading data dynamically from an API using a UISearchBar and trying to display it, using something like this:
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
NSLog(#"Search Text: %#",[searchBar text]);
[self startSearchingWithText:[searchBar text]];
}
- (void) startSearchingWithText:(NSString *)text {
...
// Go through the list of services and set up a query for the search term
QueryServiceManager *manager = [[QueryServiceManager alloc] initWithServices: services];
// Set up the query
if (![manager addKeyword: text])
NSLog(#"PROBLEM!");
// Execute the query and store the titles in an array
self.data = [[manager makeQuery] copy]; // Returns a NSArray
NSLog(#"%#",self.data);
// Add the items to the tableView
[self.tableView reloadData];
My UITableViewController code is set up to read from self.data, which is initially a NSArray of #"" elements, as such:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
[self.data count];
}
- (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] autorelease];
}
// Set up the cell...
cell.textLabel.text = [self.data objectAtIndex:indexPath.row];
return cell;
}
All I get when I do this is:
(gdb) continue
2010-02-26 18:43:32.515 iSave[11690:207] (
"Toshiba Satellite L505-GS5037 TruBrite 15.6-Inch Laptop (Black)",
"Dell Inspiron 11 11.6-Inch Obsidian Black Laptop (Windows 7 Premium)",
"ASUS Eee PC Seashell 1005PE-MU17-BK 10.1-Inch Black Netbook - Up to 11 Hours of Battery Life",
"SwissGear Computer Backpack (Red)",
"Belkin 36-Piece Demagnatized Computer Tool Kit with Case (Black)",
"Compaq EVO N610C",
"Belkin F8E062 55-Piece Computer Tool Kit with Black Case (Demagnetized Tools)",
"ASUS Eee PC Seashell 1005PE-PU17-BK 10.1-Inch Black Netbook - Up to 14 Hours of Battery Life",
"Harman Kardon SoundSticks II 2.1 Plug and Play Multimedia Speaker System",
"ION Audio VCR 2 PC USB VHS Video to Computer Converter"
)
(gdb) continue
Program received signal: “EXC_BAD_ACCESS”.
(gdb)
Any ideas? It looks like the NSArray is being populated properly, then things are failing on/after the reloadData call (breakpoints confirm this, but can't isolate where things are going wrong)
EDIT: I replaced theNSLog() with enumeration
for ( NSString *elem in self.data )
NSLog(elem);
And it still crashes. I managed to coax a log out of it: http://pastebin.com/NDVKLsJC
When I remove the NSLog() entirely, it doesn't crash, but the UITableViewdoesn't update, either.
Have you identified the object which you're trying to access but which no longer exists, the object which is giving EXC_BAD_ACCESS?
If not then you should enable Zombies in your app, then you can issue a command to gdb which will tell you which object is causing the crash.
Something weird: You're saying that you only have 1 row in each section, yet you're using indexPath.row to index into your array? One of those is wrong, and I'm guessing it's the first. Usually if you're displaying an array (of n elements) in a UITableView, you have one section and n rows in that section, which means you'd use [myArray objectAtIndex:indexPath.row] to retrieve the appropriate object.
If you want a separate section for each item in the array, then you say that you have n sections, and 1 row in each section, and then use [myArray objectAtIndex:indexPath.section] to retrieve the appropriate object.
Oh, and you're leaking memory all over the place.
What about:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.data count];
}
instead of always returning 1.
Hit shift-command-b to analyze the project and solve all problems displayed (potential leaks, etc.)