I have a question about the event value changed for UISwitch, here is my problem in detail.
in numberOfRowsInSection i have loop that calls the a data base method which return #of rows for each section.
I used an array of arrays(BECAUSE OF I HAVE MANY SECTIONS WITH MANY ROWS) that keeps state of UISwitch then update it whenever value changed called, here is the code of the event:
HOWEVER, after all of these UISwitch still resets whenever I scroll up or down. PLEASE HELP ME AS SOON AS POSSIBLE I will appreciate YOUR HELP SOOOOO MUCH .
Thank you in advance.
I think you make logic error in if (sender.on) in -(void)switchChanged:(UISwitch *)sender method because when sender.on == YES you make OFF:) write
-(void)switchChanged:(UISwitch *)sender
{
UITableViewCell *cell = (UITableViewCell *)[sender superview];
NSIndexPath *x =[mainTableView indexPathForCell:cell];
NSMutableArray *repl = [SwitchArray objectAtIndex:x.section];
[repl replaceObjectAtIndex:x.row withObject:(sender.on ? #"ON", #"OFF")];
}
You can double check the value in the table view willDisplayCell: just to make sure you have it right:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
UISwitch* uiSwitch = (UISwitch*) cell.accessoryView;
if (uiSwitch != nil && [uiSwitch isKindOfClass:[UISwitch class]]) {
//just make sure it is valid
NSLog(#"switch value at %d-%d is: %#",indexPath.section, indexPath.row, [SwitchArray[indexPath.section] objectAtIndex:indexPath.row] );
uiSwitch.on = [[SwitchArray[indexPath.section] objectAtIndex:indexPath.row] isEqualToString:#"ON"];
}
}
As an aside, you can you use NSNumbers to make the code more readable:
-(void)switchChanged:(UISwitch *)sender
{
UITableViewCell *cell = (UITableViewCell *)[sender superview];
NSIndexPath *x=[mainTableView indexPathForCell:cell];
NSLog(#"%ld", (long)x.section);
//NSLog(#"index for switch : %d", switchController.tag );
NSMutableArray *repl = repl= [SwitchArray objectAtIndex:x.section];
repl[x.section] = #(sender.on);
}
Then where you set the on value:
uiSwitch.on = [[SwitchArray[indexPath.section] objectAtIndex:indexPath.row] boolValue];
Cells get reused. You are creating a new switch every time a cell is used. You should only create a switch once for each cell. Try the following in your cellForRow... method:
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
UISwitch *switchController = [[UISwitch alloc] initWithFrame:CGRectZero];
[switchController setOn:YES animated:NO];
[switchController addTarget:self action:#selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
cell.accessoryView = switchController;
[switchController release];
}
UISwitch *switch = cell.accessoryView;
Related
I have this cellForRowAtIndexPath method here:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
if (cell.accessoryView == nil) {
cell.accessoryView = [[Checkbox alloc] initWithFrame:CGRectMake(0, 0, 25, 43)];
cell.accessoryView.opaque = NO;
cell.backgroundColor = [UIColor clearColor];
[(Checkbox*)cell.accessoryView addTarget:self action:#selector(checkBoxTapped:forEvent:) forControlEvents:UIControlEventValueChanged];
}
NSString *sectionTitle = [contactSectionTitles objectAtIndex:indexPath.section];
NSArray *sectionContacts = [contactDirectoryFinal objectForKey:sectionTitle];
NSString *contacts = [[sectionContacts objectAtIndex:indexPath.row] valueForKey:#"item"];
cell.textLabel.text = contacts;
[(Checkbox*)cell.accessoryView setChecked: [[[sectionContacts objectAtIndex:indexPath.row] valueForKey:#"checkedItem"] boolValue] ];
return cell;
}
inside this method, I have this line:
[(Checkbox*)cell.accessoryView addTarget:self action:#selector(checkBoxTapped:forEvent:) forControlEvents:UIControlEventValueChanged];
which calls this method
- (IBAction)checkBoxTapped:(id)sender forEvent:(UIEvent*)event
{
}
What I am trying to do is adjust the way this method is called so I am also passing int the indexPath.row number
I got started with this:
[(Checkbox*)cell.accessoryView addTarget:self action:#selector(checkBoxTapped:forEvent:rowNumber:) forControlEvents:UIControlEventValueChanged];
and this
- (IBAction)checkBoxTapped:(id)sender forEvent:(UIEvent*)event rowNumber:(int)row
{
}
but I am new to selectors, my question is where do I pass in the rowNumber?
As you've found out you can't pass just any values with your selector, only the sender and the event. This is why you need another way pass the row number.
One method is to use the tag property on your control and retrieve it with [sender tag], however this approach will fail if the table indexes are changed without cellForRowAtIndexPath being called. It also won't work for sectioned table views.
A more robust approach is to use the location of your view to calculate the correct row. UITableView has a convenient method to do this:
- (IBAction)checkBoxTapped:(id)sender forEvent:(UIEvent*)event {
CGPoint checkBoxPosition = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:checkBoxPosition];
}
As an alternative to passing it in ....
func checkBoxTapped(sender: CheckBox, withEvent event: UIEvent) {
if let tap = event.allTouches()?.first {
let location = tap.locationInView(tableView)
let indexPath = tableView.indexPathForRowAtPoint(location)
..... do something
}
}
*Update
As #Maddy 's comment
Do not use tag to represent the indexPath. It fails if the table view allows any rows to be moved, inserted, or deleted
this is not a good answer.*
As CheckBox is a UIView you can set its tag in cellForRowAtIndexPath: and then check for it in checkBoxTapped:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
if (cell.accessoryView == nil) {
cell.accessoryView = [[Checkbox alloc] initWithFrame:CGRectMake(0, 0, 25, 43)];
cell.accessoryView.opaque = NO;
cell.backgroundColor = [UIColor clearColor];
[(Checkbox*)cell.accessoryView addTarget:self action:#selector(checkBoxTapped:forEvent:) forControlEvents:UIControlEventValueChanged];
}
NSString *sectionTitle = [contactSectionTitles objectAtIndex:indexPath.section];
NSArray *sectionContacts = [contactDirectoryFinal objectForKey:sectionTitle];
NSString *contacts = [[sectionContacts objectAtIndex:indexPath.row] valueForKey:#"item"];
cell.textLabel.text = contacts;
[(Checkbox*)cell.accessoryView setChecked: [[[sectionContacts objectAtIndex:indexPath.row] valueForKey:#"checkedItem"] boolValue] ];
// set the index to the tag.
cell.accessoryView.tag = indexPath.row
return cell;
}
- (IBAction)checkBoxTapped:(id)sender forEvent:(UIEvent*)event
{
// get the index from the tag value
NSInteger row = ((UIView *)sender).tag
}
You could use:
- (NSIndexPath *)indexPathForCell:(UITableViewCell *)cell
once you have the UITableViewCell object which can be accessed as sender.superview
I have a custom UITableViewCell, and when it's selected, it expands and adds a UILabel to the selected cells UIView that I added in the storyBoard.
When I run the app and select a cell, the label gets added to myView as expected. The problem is, when I scroll down, the label is also shown at another cell.
Apparently the reason its behaving like so, is because I'm reusing the cell and I don't clean them as Emilie stated. I'm trying to call the method of prepareForReuse and 'cleaning' the cell, but I'm having trouble doing that. Here is my code:
- (void)prepareForReuse {
NSArray *viewsToRemove = [self.view subviews];
for (UILablel *v in viewsToRemove) {
[v removeFromSuperview];
}
Doing that, cleans even the selected cells label.
- (void)viewDidLoad {
self.sortedDictionary = [[NSArray alloc] initWithObjects:#"Californa", #"Alabama", #"Chicago", #"Texas", #"Colorado", #"New York", #"Philly", #"Utah", #"Nevadah", #"Oregon", #"Pensilvainia", #"South Dekoda", #"North Dekoda", #"Iowa", #"Misouri", #"New Mexico", #"Arizona", #"etc", nil];
self.rowSelection = -1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CategorieCell *customCell = [tableView dequeueReusableCellWithIdentifier:#"cellID" forIndexPath:indexPath];
customCell.title.text = [self.sortedDictionary objectAtIndex:indexPath.row];
return customCell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
CategorieCell *customCell = (CategorieCell *)[tableView cellForRowAtIndexPath:indexPath];
if (self.info) {
[self.info removeFromSuperview];
}
self.info = [[UILabel alloc] init];
[self.info setText:#"Hello"];
[self.info setBackgroundColor:[UIColor brownColor]];
CGRect labelFrame = CGRectMake(0, 0, 50, 100);
[self.info setFrame:labelFrame];
[customCell.infoView addSubview:self.info];
NSLog(#"%ld", (long)indexPath.row);
self.rowSelection = [indexPath row];
[tableView beginUpdates];
[tableView endUpdates];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([indexPath row] == self.rowSelection) {
return 159;
}
return 59;
}
The answer is quite simple : you reuse your cell like you should, but never clean them
Reusing your UITableViewCell means that the cell you clicked on previously will be reused when it will go off-screen.
When clicked, you add a view to your UITableViewCell. When reused, the view is still there because you never remove it.
You have two choices : One, you could set a tag of the self.info view (or check with the indexpath you're keeping in memory), then check when you dequeue the cell if the info view is there, and remove it. The cleaner solution would be to implement the view removal by overriding the prepareForReuse method of your custom UITableViewCell
Precision
The first thing you need to do is set a tag for your self.info view after initializing it:
[self.info setTag:2222];
If you want to keep it as simple as possible, you could check and remove the self.info view directly in your cellForRowAtIndexPath method :
CategorieCell *customCell = [tableView dequeueReusableCellWithIdentifier:#"cellID" forIndexPath:indexPath];
customCell.title.text = [self.sortedDictionary objectAtIndex:indexPath.row];
if [customCell.infoView viewWithTag: 2222] != nil {
[self.info removeFromSuperview]
}
return customCell;
I am not a percent sure this code compiles, I cannot test it on my side for now. Hope it works !
I am fairly new to Objective C programming, and have a UITableView setup with a custom cell. I want to make it so a user can touch a button that will add another cell, and this button will appear in the last cell only. Currently, it is not showing up. Here is the code that I am using. I have created the button within the custom cell, and used "setHidden:YES" to hide it within the cell itself. I am trying "setHidden:NO" to make the button appear in the TableView code, but it is not working. I thought maybe it had something to do with reloading the cell, but I am not sure if I am going in the right direction with this or not. I would appreciate any help on this, thanks.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{workoutTableViewCell *cell = (workoutTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
[cell.addButton setTitle:(NSString *)indexPath forState:UIControlStateApplication];
[cell.textLabel setText:[NSString stringWithFormat:#"Row %i in Section %i", [indexPath row], [indexPath section]]];
NSInteger sectionsAmount = [tableView numberOfSections];
NSInteger rowsAmount = [tableView numberOfRowsInSection:[indexPath section]];
if ([indexPath section] == sectionsAmount - 1 && [indexPath row] == rowsAmount - 1) {
NSLog(#"Reached last cell");
[cell.addButton setHidden:NO];
if (lc == NO)
{[[self tableView] reloadData];
lc = YES;
}
}
return cell;
}
Following UITableViewDataSource method will help you to return exact number of rows available in section. Here you need to return additional as you want to have last as your button.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return yourRowCount + 1;
}
Now in folowing method you will check row number using indexpath.row as
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *lastCellIdentifier = #"LastCellIdentifier";
static NSString *workoutCellIdentifier = #"WorkoutCellIdentifier";
if(indexPath.row==(yourRowCount+1)){ //This is last cell so create normal cell
UITableViewCell *lastcell = [tableView dequeueReusableCellWithIdentifier:lastCellIdentifier];
if(!lastcell){
lastcell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:lastCellIdentifier];
CGRect frame = CGRectMake(0,0,320,40);
UIButton *aButton = [UIButton buttonWithType:UIButtonTypeCustom];
[aButton addTarget:self action:#selector(btnAddRowTapped:) forControlEvents:UIControlEventTouchUpInside];
aButton.frame = frame;
[lastcell addSubview:aButton];
}
return lastcell;
} else { //This is normal cells so create your worktouttablecell
workoutTableViewCell *cell = (workoutTableViewCell *)[tableView dequeueReusableCellWithIdentifier:workoutCellIdentifier];
//Configure your cell
}
}
Or you can do like create UIView programatically and set it as FooterView as suggested by #student in comment code would look like,
CGRect frame = CGRectMake(0,0,320,40);
UIView *footerView = [[UIView alloc] initWithFrame:frame];
UIButton *aButton = [UIButton buttonWithType:UIButtonTypeCustom];
[aButton addTarget:self action:#selector(btnAddRowTapped:) forControlEvents:UIControlEventTouchUpInside];
aButton.frame = frame;
[footerView addSubView:aButton];
[yourTableNmae setTableFooterView:footerView];
Declare method as follow
-(IBAction)btnAddRowTapped:(id)sender{
NSLog(#"Your button tapped");
}
if ([indexPath section] == sectionsAmount - 1 && [indexPath row] == rowsAmount - 1) {
NSLog(#"Reached last cell");
[cell.addButton setHidden:NO];
} else {
[cell.addButton setHidden:YES];
}
Replace this code in your program.
If you know your number of cells in the uitable and you wish to just know when the last row will appear, you could implement the following delegate method
(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
this method tells the delegate table view is about to draw cell for particular row, simple compare your row with table rowcount.
This may be a long question in order I'll be able to explain all the problem I've encountered.
So, I want to implement such UI functionality:
So I have a UITableView which is implemented in one file and it's cells which is implemented in other file TableViewCell.m
And I need after clicking on button read more to expand UILabel with text mesage as on the second screen and after clicking on close message button to restore UILabel and consequently resize TableView cell (this is one button i only change images of it). So in order to resize UILabel I use [UILabel sizeToFit] and change label.numberOfLines from 3 to 0 (iOS 6 feature as far as I know).And here code of creating cells:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
CPNTalkCell *cell = (CPNTalkCell *)[tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
NSString *modelId = [[models objectAtIndex:indexPath.row] valueForKey:#"modelId"];
NSDictionary *model = [[models objectAtIndex:indexPath.row] valueForKey:#"model"];
return [self setupCell:cell forModel:model withId:modelId];
}
In setupCell method I do additional adjustments and in CPNTalkCell the cell is described vi IB.
And here the code of button ReadMore event handler where I try to resize cell and label:
-(void)ResizeLabel:(id)sender
{
UIView* sender_button = (UIView*)sender;
UIButton* _resizingSenderButton = (UIButton*)sender;
CGRect _button_before = _resizingSenderButton.frame;
NSIndexPath* indexPath = [listView indexPathForCell:(UITableViewCell*)[[ sender_button superview]superview ]]; //In such an ugly way may be i
//access NSIndexpath of the current cell from
// which button was clicked
CPNTalkCell *cell = (CPNTalkCell*)[listView cellForRowAtIndexPath:indexPath];
if (cell.messageLabel.numberOfLines==3) {
[self.listView beginUpdates];
cell.messageLabel.numberOfLines=0;
[cell.messageLabel sizeToFit];
_button_before.origin.y = cell.messageLabel.frame.origin.y+ cell.messageLabel.frame.size.height+7;
_resizingSenderButton.frame=_button_before;
[_resizingSenderButton setBackgroundImage:[UIImage imageNamed:#"readmore2.png" ] forState:UIControlStateNormal];
[cell sizeToFit];
// [self.listView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];
[self.listView reloadData];
[self.listView endUpdates];
}
else
{
cell.messageLabel.numberOfLines = 3;
[self.listView beginUpdates];
[cell.messageLabel sizeToFit];
CGRect _button_after = _resizingSenderButton.frame;
_button_after.origin.y = cell.messageLabel.frame.origin.y+ cell.messageLabel.frame.size.height+7;
_resizingSenderButton.frame=_button_after;
[_resizingSenderButton setBackgroundImage:[UIImage imageNamed:#"readmore1.png" ] forState:UIControlStateNormal];
[cell sizeToFit];
[self.listView reloadData];
//[self.listView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];
[self.listView endUpdates];
}
}
So this code work and after clicking cells resize to fit the text and it's length but works strange - sometimes buttons disappear or appear on the text after scrolling the list buttons also may disappear or even text.I use reloadData when I change cell - i know it's not optimal, but as you see I've commented code of reloading of one cell because it works even more strange or not from the first clicking.
I also overload heightForRowatIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *modelId = [[models objectAtIndex:indexPath.row] valueForKey:#"modelId"];
NSDictionary *model = [[models objectAtIndex:indexPath.row] valueForKey:#"model"];
CPNTalkCell *cell = [_cells objectForKey:modelId];
if (cell == nil) {
cell = [self setupCell:nil forModel:model withId:modelId];
}
return cell.rowHeight;
}
But in it i describe initial cell parameters before clicking on a button via calling of rowHeight described in TalkCell file!
I know it's a long question and I sometimes explain not all clear but I think iOS experts will be able to understand my problem and propose solution of this problem. I really need a help because I try to solve this problem not for a one day.Great thanks in advance!
You need to overload heightForRowAtIndexPath to return the appropriate value based on the expanded cell
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *mode
lId = [[models objectAtIndex:indexPath.row] valueForKey:#"modelId"];
NSDictionary *model = [[models objectAtIndex:indexPath.row] valueForKey:#"model"];
CPNTalkCell *cell = [_cells objectForKey:modelId];
if (cell == nil) {
cell = [self setupCell:nil forModel:model withId:modelId];
}
if ( indexPath == expandedIndexPath )//save the expanded cell's index path somewhere
{
return expandedCellHeight;
}
return cell.rowHeight;
}
Sorry I'm pretty new to iOS dev.
I have a UITableView setup from cells being pulled from a single XiB nib. I've created a on/off switch in the nib, and I am trying to save the state of the switch upon viewWillDisappear for the number of cells that I have. (6 cells to be exact).
How can I loop through all the cells and save this information?
I tried this in my UIViewController to get the info for one cell:
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
UITableView *tv = (UITableView *)self.view;
UITableViewCell *tvc = [tv cellForRowAtIndexPath:0];
}
it gives me the error "Program received signal: "EXC_BAD_INSTRUCTION".
How can I accomplish this?
You have to pass a valid NSIndexPath to cellForRowAtIndexPath:. You used 0, which means no indexPath.
You should use something like this:
UITableViewCell *tvc = [tv cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
BUT. Don't do this. Don't save state in the UITableViewCell.
Update your dataSource when a switch changed its state.
If you have implemented the UITableViewDataSource methods the right why your tableView reuses cells. That means the state of your cells will vanish when the cells are reused.
Your approach might work for 6 cells. But it will fail for 9 cells.
It will probably even fail if you scroll the first cell off screen.
I wrote a quick demo (if you don't use ARC add release where they are necessary) to show you how you should do it instead:
- (void)viewDidLoad
{
[super viewDidLoad];
self.dataSource = [NSMutableArray arrayWithCapacity:6];
for (NSInteger i = 0; i < 6; i++) {
[self.dataSource addObject:[NSNumber numberWithBool:YES]];
}
}
- (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];
UISwitch *aSwitch = [[UISwitch alloc] init];
[aSwitch addTarget:self action:#selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
cell.accessoryView = aSwitch;
}
UISwitch *aSwitch = (UISwitch *)cell.accessoryView;
aSwitch.on = [[self.dataSource objectAtIndex:indexPath.row] boolValue];
/* configure cell */
return cell;
}
- (IBAction)switchChanged:(UISwitch *)sender
{
// UITableViewCell *cell = (UITableViewCell *)[sender superview];
// NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
CGPoint senderOriginInTableView = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:senderOriginInTableView];
[self.dataSource replaceObjectAtIndex:indexPath.row withObject:[NSNumber numberWithBool:sender.on]];
}
as you see it's not very complicated to not store state in the cells :-)
Moving [super viewDidDisappear:animated]; to the end of your method may be the most expedient way to address the problem. If that does not work, move the logic into viewWillDisappear:animated:.
A better way to deal with this would be to avoid reading the current state from the view at all. Rather, the view should pass the state to the model on each update. This way you would be able to harvest the current state from your model, entirely independently from the state of your view.