I am seeing some weird things happening with viewDidLayoutSubviews and UITableViewCells.
I have 2 tables on a page, both of which get sized so they do not scroll (all elements are visible). I take care of the resizing in viewDidLayoutSubviews like this:
- (void)viewDidLayoutSubviews {
self.orderItemTableViewHeightConstraint.constant = self.orderItemTableView.contentSize.height - [self tableView:self.orderItemTableView heightForHeaderInSection:0];
self.shippingOptionTableViewHeightConstraint.constant = self.shippingMethodTableView.contentSize.height;
[self.view layoutIfNeeded];
self.scrollViewContainerViewHeightConstraint.constant = self.shippingMethodTableView.$bottom;
[self.view layoutIfNeeded];
}
This works as expected.
However, when shippingOptionTableView is built, it has 3 cells (for this example), each with a model that feeds the label in the cell and a selection marker like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.shippingMethodTableView) {
DLShippingOptionTableCell *cell = [tableView dequeueReusableCellWithIdentifier:SHIPPING_OPTION_CELL_IDENTIFIER];
if (cell == nil) {
NSString *nibName = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) ? #"DLShippingOptionTableCell~iphone" : #"DLShippingOptionTableCell";
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:nibName owner:nil options:nil];
for (id currentObject in topLevelObjects) {
if ([currentObject isKindOfClass:[DLShippingOptionTableCell class]]) {
cell = (DLShippingOptionTableCell *)currentObject;
[cell setupCell];
break;
}
}
}
cell.model = [self.model.shippingOptionArray objectAtIndex:indexPath.row];
return cell;
}
else {
...
}
}
The model gets set in the table cell, which causes the visual state to be updated:
- (void)setModel:(DLShippingOption *)model {
_model = model;
NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
NSString *numberAsString = [currencyFormatter stringFromNumber:[NSNumber numberWithFloat:(model.priceInPennies / 100.0f)]];
self.shippingLabel.text = [NSString stringWithFormat:#"%# (%#)", model.optionName, numberAsString];
[self toggleSelectedState:model.isSelected];
}
- (void)toggleSelectedState:(BOOL)isSelected {
[self setSelected:isSelected];
self.radioButtonImageView.image = isSelected ? [UIImage imageNamed: #"radio_button_selected.jpg"] : [UIImage imageNamed: #"radio_button_unselected.jpg"];
}
Here is the problem... the table cell get sent setSelected:NO repeatedly during the layout process. So, even when my model is set to selected, and I update the radio-button graphic (which I would normally do in setModel:) it gets overridden and changed to false.
I had to put this hack in place to make it work (basically adding this bit of code to the cellForRowAtIndexPath in the ViewController, just after setting the cell's model:
if (cell.model.isSelected) {
[self.shippingMethodTableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
I this bites... So the question is, why are table cells getting sent setSelected:NO over and over and over during layout, and is there a better way to overcome that?
Try using prepareForReuse method and see if it helps. It will be also helpful to see the documentation here:
https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewCell_Class/Reference/Reference.html
If a UITableViewCell object is reusable—that is, it has a reuse identifier—this method is invoked just before the object is returned from the UITableView method dequeueReusableCellWithIdentifier:. For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view's delegate in tableView:cellForRowAtIndexPath: should always reset all content when reusing a cell. If the cell object does not have an associated reuse identifier, this method is not called. If you override this method, you must be sure to invoke the superclass implementation.
Also note that you should only use this method for stuff which is not related to content as pointed out in the documentation. Content related stuff should always be done in tableView:cellForRowAtIndexPath: method
Related
This is my error:
-[__NSArrayM objectAtIndex:]: index 12 beyond bounds for empty array
I know this error means I'm trying to access an "empty array".
This error only happens in viewX when it is popped back from viewY. When you press 'back button' on navigation bar in viewY and scroll the tableView immediately, it will crash and cause this error. I am using the RETableViewManager to load my tableView.
In viewX's viewDidLoad:
[[RACSignal combineLatest:#[RACObserve(self, record), RACObserve(self, restaurant)]] subscribeNext:^(id x) {
[self setupItems];
}];
in setupItems:
RecordManager *recordManager = [[EZRecordManager alloc] initWithRecord:self.record restaurant:self.restaurant sender:self.navigationController];
self.items = [recordManager items];
self.section = [RETableViewSection section];
[self.items each:^(id data) {
if ([data isKindOfClass:[NSString class]]) {
self.navigationItem.title = (NSString *)data;
} else {
[self registerItem:[data class]];
[self.section addItem:data];
}
}];
[self.manager addSection:self.section];
[self.tableView reloadData];
I NSLogged my array 'self.items'. and this is what logs according to the method:
viewDidAppear - (
"\U5df2\U8a02\U4f4d\Uff0c\U5c1a\U672a\U7528\U9910",
"<REReservationHeaderItem: 0x14015b0b0>",
"<REAttributedStrItem: 0x14015b1b0>",
"<REAttributedStrWithNextItem: 0x140191a70>",
"<REAttributedStrItem: 0x140193f60>",
"<RESpacerItem: 0x140194870>",
"<REAttributedStrWithNextItem: 0x14019ce10>",
"<REAttributedStrItem: 0x140199230>",
"<RESpacerItem: 0x1401a04e0>",
"<REActionItem: 0x14019e490>",
)
The NSLog logs the same array in setupItems so I know the array is still there because self.item is saved as a property:
#property (nonatomic, strong) NSArray *items;
So this algorithm works as expected when I'm loading viewX for the first time, but as soon as I go into another view(viewY) and press the 'back button' on viewY to pop to viewX and then immediately scroll, it crashes with the above error. If I wait for a second (maybe even half a second), viewX will work properly and have no issue. I know this is minor but my PM is stressing that this shouldn't happen. How can I solve this problem?
The method the error occurs in (part of the RETableViewManager library):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
RETableViewSection *section = [self.mutableSections objectAtIndex:indexPath.section];
RETableViewItem *item = [section.items objectAtIndex:indexPath.row];
UITableViewCellStyle cellStyle = UITableViewCellStyleDefault;
if ([item isKindOfClass:[RETableViewItem class]])
cellStyle = ((RETableViewItem *)item).style;
NSString *cellIdentifier = [NSString stringWithFormat:#"RETableViewManager_%#_%li", [item class], (long) cellStyle];
Class cellClass = [self classForCellAtIndexPath:indexPath];
if (self.registeredXIBs[NSStringFromClass(cellClass)]) {
cellIdentifier = self.registeredXIBs[NSStringFromClass(cellClass)];
}
if ([item respondsToSelector:#selector(cellIdentifier)] && item.cellIdentifier) {
cellIdentifier = item.cellIdentifier;
}
RETableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
void (^loadCell)(RETableViewCell *cell) = ^(RETableViewCell *cell) {
cell.tableViewManager = self;
// RETableViewManagerDelegate
//
if ([self.delegate conformsToProtocol:#protocol(RETableViewManagerDelegate)] && [self.delegate respondsToSelector:#selector(tableView:willLoadCell:forRowAtIndexPath:)])
[self.delegate tableView:tableView willLoadCell:cell forRowAtIndexPath:indexPath];
[cell cellDidLoad];
// RETableViewManagerDelegate
//
if ([self.delegate conformsToProtocol:#protocol(RETableViewManagerDelegate)] && [self.delegate respondsToSelector:#selector(tableView:didLoadCell:forRowAtIndexPath:)])
[self.delegate tableView:tableView didLoadCell:cell forRowAtIndexPath:indexPath];
};
if (cell == nil) {
cell = [[cellClass alloc] initWithStyle:cellStyle reuseIdentifier:cellIdentifier];
loadCell(cell);
}
if ([cell isKindOfClass:[RETableViewCell class]] && [cell respondsToSelector:#selector(loaded)] && !cell.loaded) {
loadCell(cell);
}
cell.rowIndex = indexPath.row;
cell.sectionIndex = indexPath.section;
cell.parentTableView = tableView;
cell.section = section;
cell.item = item;
cell.detailTextLabel.text = nil;
if ([item isKindOfClass:[RETableViewItem class]])
cell.detailTextLabel.text = ((RETableViewItem *)item).detailLabelText;
[cell cellWillAppear];
return cell;
}
Usually when "waiting a little fixes the problem", it's because you have an async problem.
Something to check first :
Make sure your reload code is called when you move back. Maybe your tableview didn't get emptied, but the array did. Moving back would let you scroll the old content (still loaded) but the delegate methods won't be able to create new cells because the array is now empty.
If you wait, your async method does it's job and the array is now full again, which makes everything work fine.
Possible solution :
Empty then reload the tableview in viewWillAppear. This will cause a visual flash of the tableview going empty and then full again. It will also scroll you to the first element. That being said, it's really easy and fast, and with a spinner it will appear much smoother.
Other possible solution :
Keep the data loaded after leaving the page, so when you come back it's still there. You can use anything that will keep the data loaded while in the app. It could be a singleton class that stays instantiated, or save in a database and reload from it (it's much faster than straight up loading from the internet), or anything that you can think of.
I have a UITableView filled with cells from a NIB-based subclass of UITableViewCell. I obtain each one like this:
+(id) getClassObjectFromNib:(NSString*) nibName subclassOf: (Class) cls owner:(id)own
{
id result = nil;
NSArray* topLevelObjects = [[NSBundle mainBundle]
loadNibNamed:nibName
owner:own
options:nil];
for ( id currentObject in topLevelObjects )
{
if ([currentObject isKindOfClass:cls])
{
result = currentObject;
[result retain];
break;
}
}
return result;
}
My call looks like:
#interface TargetViewController : UITableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *CellIdentifier = [TargetCell defaultReuseIdentifier];
TargetCell* cell = (TargetCell*) [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = (TargetCell*) [UtilityHelper getClassObjectFromNib:CellIdentifier subclassOf:[UITableViewCell class] owner:self];
}
if ( nil != cell )
{
// Other initialization code for cell controls
cell.showsReorderControl = 1;
}
return cell;
}
But 'dealloc' never gets called on the cells when their view unloads. If I remove the 'retain' above, dealloc gets called, but the app crashes when a cell is deleted individually (via swipe) from the UITableView (crash due to message to deleted item).
Except for the single deletion case, releasing the items occurs property when the view unloads. The crash is "-[TargetCell _setDeleteAnimationInProgress:]: message sent to deallocated instance".
I discovered the problem was due to reloading the table view as part of the row deletion operation (in commitEditingStyle).
[self.tableView reloadData];
This appears to work for standard UITableViewCells but does not work when the cell is a subclass loaded from an XIB. Now I just alter the data source, delete the row (using deleteRowsAtIndexPaths) and return.
Edit 1
To be clear, [self loadObjects] is not my method it is a method on the PFQueryTableViewController class supplied by parse to pull in new data.
I suspect this might be being caused by the table drawing code, as the tablecellview is configured to be auto-adjust it's height.
Here is the table drawing code:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
object:(PFObject *)object
{
//Setup estimated heights and auto row height
self.tableView.estimatedRowHeight = 68.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
//Give the cell a static identifier
static NSString *cellIdentifier;
socialPost* post = object;
//Check to see what sort of cell we should be creating, text, image or video
if (object[#"hasImage"] != nil) {
cellIdentifier = #"posts_with_image";
} else {
cellIdentifier = #"posts_no_image";
}
//Create cell if needed
hashtagLiveCellView *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[hashtagLiveCellView alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:cellIdentifier];
}
// Configure the cell to show our imformation, loading video and images if needed
cell.postTitle.text = [NSString stringWithFormat:#"#%#",object[#"userName"]];
cell.postText.text = [NSString stringWithFormat:#"%#",
object[#"text"]];
[cell.userImage setImageWithURL:[NSURL URLWithString:post.userImageURL]];
[cell.postImage setImageWithURL:[NSURL URLWithString:post.imageURL]];
//Set ID's on the custom buttons so we know what object to work with when a button is pressed
cell.approveButtonOutlet.stringID = object.objectId;
[cell.approveButtonOutlet addTarget:self action:#selector(approvePostCallback:) forControlEvents:UIControlEventTouchUpInside];
cell.deletButtonOutlet.stringID = object.objectId;
[cell.deletButtonOutlet addTarget:self action:#selector(deletePostCallback:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
Original
I have a PFQueryTableViewController that i am loading with object from parse.
I have a scheduled task set to run every 20 seconds that calls:
[self loadObjects]
To fetch any new objects, or any changed to objects that have happened.
That all works fine, however if i am scrolled halfway down the tableview when the loadObjects is called the page jumps back to the top. Even if there are no new or changed data available.
Is there an easy way around this, before i start looking into hacky ways to catch the reload and force the table to stay where it is.
Thanks
Gareth
When you're calling loadObjects you load the objects from start. And there for you get the first results again.
Try to change [self loadObjects]; to [self.tableView reloadData];.
I have a table that imports the address book contacts and displays the image, contact name and a checkbox, i've customized my cell with image, label and a button which serves as a checkmark. I'm successfully able to display all contacts and the checkmark also works fine for individual cells, but i'm having trouble implementing select all functionality that will put all the button in the table in selected state and clicking it again will deselect it all. This is what I have done so far.
//This is the class for my custom cell
#import "InviteFriendsCell.h"
#implementation InviteFriendsCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
}
return self;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
}
//This is my checkmark button
-(IBAction)onAddTouched:(id)sender
{
UIButton *addButton = (UIButton *)sender;
[addButton setSelected:![addButton isSelected]];
}
#end
//This is my tableview display code in FriendListViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"friendsCell";
InviteFriendsCell *cell= [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell==nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"InviteFriendsCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
// Configure the cell...
NSUInteger row = 0;
NSUInteger sect = indexPath.section;
for (NSUInteger i = 0; i < sect; ++ i)
row += [self tableView:tableView numberOfRowsInSection:i];
row += indexPath.row;
cell.thumbImage.layer.cornerRadius=25;
cell.thumbImage.clipsToBounds=YES;
cell.thumbImage.image=nil;
self.persons = CFArrayGetValueAtIndex(self.contactAdd, row);
NSString *tweet=[[NSString stringWithFormat:#"%#",(__bridge_transfer NSString *)ABRecordCopyCompositeName(self.persons)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
[cell.friendName setText:tweet];
if (ABPersonHasImageData(persons))
{
NSData *imgData = (__bridge NSData *) ABPersonCopyImageDataWithFormat(persons, kABPersonImageFormatThumbnail);
cell.thumbImage.image = [UIImage imageWithData:imgData];
}
else
{
cell.thumbImage.image = [UIImage imageNamed:#"name_icon.png"];
}
return cell;
}
//This is my select all button which should put checkmark to all button in tableview
- (IBAction)selectAll:(UIButton *)sender {
//This is where I need to implement the code
}
There is a similar question in stack overflow here Need to create a select all button for UITableView and add selections to an array in iOS
But here the checkmark button is created inside the tableview method, my case is different and I'm unable to implement that code. Is there a better way to do this? I'm using storyboard and xcode 5 and should work for ios 5 or above.
first create a array with boolean values
#property (retain, nonatomic) NSMutableArray *selectedArray;
Initialise your selected array based on the contact add count, better generalise it and store it into an NSArray if u are planning to expand this. Here peopelist is my NSArray
self.peopleList=(__bridge NSArray *)(self.contactAdd);
self.selectedArray=[[NSMutableArray alloc]initWithCapacity:[self.peopleList count]];
for(int i=0;i<[self.peopleList count];i++)
{
[self.selectedArray insertObject:[NSNumber numberWithBool:NO] atIndex:i];
}
In the selectAll button action use this
- (IBAction)selectAll:(UIButton *)sender {
for(int i=0;i<self.selectedArray.count;i++)
[self.selectedArray replaceObjectAtIndex:i withObject:[NSNumber numberWithBool:sender.isSelected]];
[sender setSelected:!sender.isSelected];
[yourTable reloadData];
}
You need to change your data source and your cell. contactAdd needs to know that the contact is selected (you could use a new array for this, but why would you). The cell needs to callback to the controller when the button is tapped to tell the controller about whether it's selected or not. The controller needs to update the cells to set the selected status.
Your code currently won't work properly if you have lots of cells, select some and then scroll (either other rows will show as selected or the selections will be lost depending on if your reuse identifier is configured correctly).
Also, your contactAdd is overly complex. Look at using multiple arrays (one per section) as your current indexing is complicated to maintain.
I really think that using predefined "selected" method is bad and rigid approach. So I would prefer the following solution:
Just connect touch listener to every cell and handle the Tap event. When it occurs set the cell data source "selected" and update the layout of the cell.
If you want to "select all" you should update all cell datasources to "selected" and then update the whole table (reloadData)
Note: instead of making one template and manipulating data you can also create two templates for the cell - selected and unselected one and implement custom template selector in your cell constructor.
Before I post the question itself, I need to state this is a jailbreak app. This is why I'm writing in "bizarre" folders in the filesystem.
Let's continue.
Here is my cellForRowAtIndexPath method:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"pluginCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier];
}
if(indexPath.row == 0)
{
cell.textLabel.text = #"default";
}else
{
//Get the plugin's display name.
NSBundle *currentPlugin = [[NSBundle alloc] initWithPath:[NSString stringWithFormat:#"/Library/Cydeswitch/plugins/%#", [plugins objectAtIndex:indexPath.row - 1], nil]];
cell.textLabel.text = [[currentPlugin localizedInfoDictionary] objectForKey:#"CFBundleDisplayName"];
if(cell.textLabel.text == nil)
{
//No localized bundle, so let's get the global display name...
cell.textLabel.text = [[currentPlugin infoDictionary] objectForKey:#"CFBundleDisplayName"];
}
[currentPlugin release];
}
if([[[cell textLabel] text] isEqualToString:[settings objectForKey:#"pluginToExecute"]])
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
currentCell = [cell retain];
}
return cell;
}
Like you can see, this method uses a member called currentCell to point to the cell that is currently "selected". This is an options table and the user should be able to have only one cell with the Checkmark accessory icon at any time.
When the use selects another cell, he is changing an option and the Checkmark is supposed to disappear from the current cell and appear in the newly appeared cell. I do that like this:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
currentCell.accessoryType = UITableViewCellAccessoryNone;
[currentCell release];
currentCell = [[self tableView:tableView cellForRowAtIndexPath:indexPath] retain];
NSLog(#"CURRENT CELL %#", currentCell.textLabel.text);
currentCell.accessoryType = UITableViewCellAccessoryCheckmark;
}
But it doesn't work. The moment I tap another cell, the Checkmark correctly disappears from the old cell, but it never shows up in the new cell.
I know the selection work fine because that NSLog there prints the new cell's text just fine.
I have tried keeping track of the indexPath before, but it didn't work at all. When I tried using indexPaths instead of pointers to cells, when the user tapped the cell nothing happened at all (at least with my current approach the checkmark disappears from the old cell).
I think it has something to do with cellForRowAtIndexPath because if I keep pointing at the cells the checkmark disappears, but for some reason when trying to change the accessory type from a cell fetched with cellForRowAtIndexPath it doesn't seem to work at all.
Any help will be appreciated.
Typo? Try this:
currentCell = [[self.tableView cellForRowAtIndexPath:indexPath] retain];
You mustn't keep track of the last selected cell the way you are. Cell's get reused. Use an ivar to keep track of the indexPath or some other key appropriate to your data.
Then in the didSelect... method you get a reference to the old cell using the saved indexPath or key. In the cellForRow... method you need to set the proper accessoryType based on whether the current indexPath matches your saved indexPath.
Lastly, do not call your own delegate/data source method. When getting a reference to a cell, ask the table view for it directly.
BTW - you are over-retaining currentCell in your cellForRow... method. There is no need to retain it all in that method unless it is the first time you are making the assignment.