I have a created a tableview cell in the nib file and added a button to it, and created an outlet to the button named as actionButton. Now, based on some condition, I want the button to be hidden or unhidden. I used the code below, so when the model, object.hasButton property is YES, I unhide the button and show otherwise. This code looks simple to me and I dont think that there should be a reuse issue, since it has either/else condition, so it should hide for the false boolean and unhide for the true boolean conditions. But, all the cell, no matter what their value is shows the button. Could somebody please help me, I have been trying to debug this but I dont seem to figure out the problem.
- (UITableViewCell*)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyObject * object = [[self.tableData objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:CELL_IDENTIFIER forIndexPath:indexPath];
cell.delegate = self;
if (cell == nil){
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CELL_IDENTIFIER];
cell.delegate = self;
}
cell.tableItem = object;
UIButton *button = cell.actionButton;
if(object.hasButton){
[button setHidden:NO];
}else{
[button setHidden:YES];
}
return cell;
}
It seems like the problem was with threading. I was doing some operation inside the managedObjectContext performBlock:andWait method like this,
[newChildContext performBlockAndWait:^{
count = [newChildContext countForFetchRequest:req error:NULL];
if(count > 0)
hasButton = YES;
else
hasButton = NO;
}];
And then updating the model like this,
myObject.hasButton = hasButton;
May be this was the problem, so I wrapped it inside the #synchronized(myObject) block to update the hasButton bool and it seems to be fine now.
#synchronzied(myObject){
myButton.hasButton = hasButton;
}
Could it be this thing ?
calling if (object.hasButton) simply checks if the property hasButton exists. it probably does exist, so it returns YES!
What you want is to check the value stored in that property, like this:
if (object.hasButton == YES)
If your MyObject is a subclass of NSManagedObject then it's hasButton property type is NSNumber * instead of BOOL. Since that NSNumber is an existing object - non nil - regardless if it's value is YES or NO myObject.hasButton evaluates to TRUE in boolean expression. Use [myObject.hasButton booleanValue] instead. also on setting that value use anObject.hasButton = #(hasButton).
Related
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
I am creating an app in where I need to add a bunch of sliders in a Dynamic UITableView. I added the slider like this:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
UISlider *slider = [[UISlider alloc]init];
//ALL OTHER CODE
[cell addSubview:slider];
return cell;
}
Now the slider is added to the UITableView but the if I changed the value of the first slider another slider changes with it.I know this is something to do with dequeing the cell but how do I fix it?
EDIT:
I tried #daveMack answer like this:
CustomCell.m:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
self.checkBox = [[M13Checkbox alloc]initWithTitle:#"Checkbox!"];
self.checkBox.checkAlignment = M13CheckboxAlignmentLeft;
[self addSubview:self.checkBox];
}
return self;
}
Cell For Row At Index Path:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CheckboxCell *cell;
cell = [self.tableView dequeueReusableCellWithIdentifier:#""];
if (!cell) {
cell = [[CheckboxCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"];
return cell;
}
return cell;
}
No, no, no! I would not suggest you add views like that to your UITableViewCell. Funky stuff can happen as you have experienced yourself because of the dequeueing process.
I suggest you do the following:
Create a custom table view cell with its appropriate .h, .m file and .xib file
In your custom cell you can add WHATEVER views you like and however many views that you like.
Make sure you create a property of type UIScrollView in your .h file and link it to the interface builder to your custom cell's slider, call the property slider.
Now in your main view controller where you are creating your table, make sure you have an NSMutableArray and name it something like sliderValuesArray that can store all your slider values for each cell. You want to make sure that the number of cells is equal to the number of elements in your sliderValuesArray.
Then you can do something like this in your cellForRowAtIndexPath delegate method:
In your cellForRowAtIndexPath method do something like this:
myCustomCell.slider.maximumValue = 100;
myCustomCell.slider.minimumValue = 1;
myCustomCell.slider.continuous = TRUE;
//set a method which will get called when a slider in a cell changes value
[myCustomCell.slider addTarget:self action:#selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
//Keep a reference to each slider by assigning a tag so that we can determine
//which slider is being changed
myCustomCell.slider.tag = indexPath.row;
//Grab the value from the sliderValuesArray and set the slider knob to that position
myCustomCell.slider.value = [[sliderValuesArray objectAtIndex:indexPath.row] intValue];
Now in your sliderChanged method you can do this:
-(void)sliderChanged:(UISlider)sender{
//Grab the slider value, it needs to be converted to an NSNumber so that we can
//store it successfully as an object in our sliderValuesArray
NSNumber sliderValue = [NSNumber numberWithInt:sender.value];
//This is how we determine which position in our slidersArray we want to update,
//Based on the tag we set our slider view on initialisation in our cellForRowAtIndexPath
int cellPosition = sender.tag;
//Use the cellPosition to update the correct number in our sliderValuesArray
//with the sliderValue retrieved from the slider that the user is sliding
[sliderValuesArray replaceObjectAtIndex:cellPosition withObject:sliderValue];
}
I ran into this same issue awhile back. The solution I came up with was to subclass UITableViewCell. Then I added the slider in the init method of the subclass and exposed it via a property.
Now, when you want to change the value of JUST ONE slider, you would do something like:
[cell slider]setValue:(someValue)];
my App is kind of a FileManager like the Finder under MAC OS. I can show up the root of the filesystem. But the Navigation through the directories makes problems.
In the method didSelectRowAtIndexPath i create a new instance of the current TableViewController. I set the Delegate and DataSource toself. Now the Problem that i have: The TableViewCells of the new created instance do not have a resueIdentifier. The value is NULL. And i cant set it because its read-only. So, how can i get the new cells to have a specific reuseIdentifier?
Below is my Code from the didSelectRowAtIndexPath- Method
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if(cell.accessoryType == UITableViewCellAccessoryDisclosureIndicator)
{
NSLog(#"Entered Directory");
NSLog(#"reuseIdentifier: %#",cell.reuseIdentifier); // <- value: "fileCell"
FileTableViewController *newDir = [[FileTableViewController alloc] init];
newDir.tableView.dataSource = self;
newDir.tableView.delegate = self;
[newDir setDirectory:cell.textLabel.text];
UITableViewCell *newCell = [newDir.tableView cellForRowAtIndexPath:indexPath];
NSLog(#"reuseIdentifier NewCell: %#",newCell.reuseIdentifier); // <- is NULL
//[newDir setTitle: [fileManager currentDirectoryPath]];
/*[self setDirectory:cell.textLabel.text];
self.title = [NSString stringWithFormat:#"%#", [fileManager currentDirectoryPath]];
[self.tableView reloadData];
*/
[self.navigationController pushViewController:newDir animated:YES];
}
}
if you need more Code to help me, just let me know.
Thanks, Chris
Maybe I'm too late with this answer. But here're my thoughts.
UITableViewCell *newCell = [newDir.tableView cellForRowAtIndexPath:indexPath];
NSLog(#"reuseIdentifier NewCell: %#",newCell.reuseIdentifier); // <- is NULL
It is NULL because the tableView is not created. It will be created just after you make a push.
If you want to pass some params to your table, you should implement the variables in the FileTableViewController.h, and then in your didSelectRowAtIndexPath make use of this like:
FileTableViewController *newDir = [[FileTableViewController alloc] init];
newDir.SomeData = cell.textLabel.text;
One more important thing - if the FileTableViewController is a UITableViewController, you should not set the dataSource and delegate, especially in your current viewController. Because doing this, you set delegate to self of current viewController (not the FileTableVC that was created).
Otherwise, if FileTableVC is not UITAbleViewController Class, you should make the delegate and datasource in FileTableVC's viewDidLoad.
I am populating a tableview from data that is received from a server. The data is a list of user activities within a given timeframe. One such activity is "Login". I do not wish to populate my tableview with this string but I'm not sure how to skip it when populating my tableview.
Here is how I populate the cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{ static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
#try{
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString *action = [object valueForKey:#"theActionName"];
if ([action isEqualtoString:#"Login"]) {
return cell;
}
return cell;
}#catch (NSException *ex) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
return cell;
}
}
As you can see I tried using return cell but as you probably know it gives me a blank cell when the table is displayed. I'm sure there is a simple line of code for this but I came up blank with the search terms I used. Could someone please enlighten me! Thanks!
P.S. you may be thinking I am not putting anything in any of the cells but I pulled out a bunch of code to keep this short.
UPDATE:
Thanks for the heads up on "isEqualtoString:" Everything worked fine with "isEqual" but I changed it given that I received so many suggestions to do so. But this is not what I am asking.
To be more clear if I had an array containing the terms: view, view, login, view. When my tableview was populated I would have 4 cells that said; view, view, login, view. I simply want to ignore the term login so that I would have 3 cells that all said view. Thanks!
There can be many way to do this.
I Belive that UITabelView should display what its datasource (here datasource is self.fetchedResultsController) contains.
What you can do is create another NSArray from self.fetchedResultsController which does not contain this object.
Try this:
NSMutableArray *newSource = [[NSMutableArray alloc] init];
for(int i = 0; i < self.fetchedResultsController.count ; i++)
{
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString *action = [object valueForKey:#"theActionName"];
if (![action isEqual:#"Login"])
{
[newSource addObject:action];
}
}
[tableView reloadData];
Now use newSource instead of self.fetchedResultsController
You might think that using one more array is not good. But believe it it is far easier than using the same array with condition. You don't have to worry about that condition when you perform some operation with your UITableView like remove object by using indexpath.
try using if ([action isEqualToString:#"Login"])
When you want to compare strings you need to use this isEqualToString .
Change this line
if ([action isEqualToString:#"Login"]) {
return cell;
}
You are using the wrong function to compare your input string and the given data variable.
They both are NSString objects so use :
if([action isEqualToString:#"Login"])
{
//enter your code here
}
#Ben : I am assuming that you have registered you cell through nib as you are using dequeueReusableCellWithIdentifier.
Make your tableview content as "Dynamic prototype" (You can see this in Attributes Inspector of table view) and change your table view cell style as custom (You can see this in Attributes Inspector of tableview cell).
I currently have a table with 8 rows that each have a label on the right side and a button on the left. I was hoping that I could have all the buttons hidden until the user presses an "edit" button in the top right corner and then they would appear allowing the user to interact with each table cell. I don't know if this is possible, because they are in UITableViewCells or if there is an easier method to summoning a button for each cell
UPDATE
okay so I have placed in all the hidden properties and there seem to be no errors, but the app doesn't recognize any of it. The buttons remains unhidden despite the fact that they are set to be initially hidden. Here is my code
Here is my Table Cell code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"BlockCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = #"Free Block";
UIButton*BlockButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
BlockButton.frame = CGRectMake(225.0f, 5.0f, 75.0f, 35.0f);
[BlockButton setTitle:#"Change" forState:UIControlStateNormal];
[BlockButton addTarget:self action:#selector(Switch:) forControlEvents:UIControlEventTouchUpInside];
Blockbutton.backgroundColor = [UIColor colorWithRed:102/255.f
green:0/255.f
blue:51/255.f
alpha:255/255.f];
Blockbutton.hidden = YES;
[cell addSubview:BlockButton];
return cell;
}
and here is my method code:
- (IBAction)Editmode:(UIButton *)sender
{
Blockbutton.hidden = !Blockbutton.hidden;
[self.tableView reloadData];
}
any thoughts or ideas as to what might be the issue?
You'll need to create a UITableViewCell subclass if you don't already have one. In that class, override setEditing:animated: and if the new value is YES, then enable/add/unhide the button.
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
if (editing) {
// add your button
someButton.hidden = NO;
} else {
// remove your button
someButton.hidden = YES;
}
}
It would be optional, but you are encouraged to animate the change if animated is YES.
Note: this assumes you have the edit button already hooked up the change the editing mode of the UITableView. If you don't, call setEditing:animated: on the UITableView in the button action. This will automatically call setEditing:animated: on each visible table cell.
The trick here is to keep in mind that a table's cells are determined by cellForRowAtIndexPath:. You can cause that method to be called all over again by sending the table reloadData:.
So, just keep a BOOL instance variable / property. Use the button to toggle that instance variable and to call reloadData:. If, at the time cellForRowAtIndexPath: is called, the instance variable is YES, set the button's hidden to YES; if NO, to NO.
take a BOOL variable which defines the whether to show delete button or not, use this BOOL var to for btnName.hidden = boolVar, initially make boolVar = NO, when user taps on edit toggle bool var and reload the tableview.
Another option is to test if you are in edit mode in the cellForRowAtIndexPath method.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = //(obtain your cell however you like)
UIButton *button = cell.button; //(get button from cell using a property, a tag, etc.)
BOOL isEditing = self.editing //(obtain the state however you like)
button.hidden = !isEditing;
return cell;
}
And whenever you enter editing mode, reload tableView data. This will make the table view ask for the cells again, but in this case the buttons will be set not to hide.
- (void)enterEditingMode {
self.editing = YES;
[self.tableView reloadData];
}