1

I am trying to figure out the best approach for adding and managing a UISwitch to my UITableView's headers, and I've read several links so far on how best to use the UISwitch in a table: here, here and here. These links demonstrate using the UISwitch in the cell whereas I'm using them in a custom UIView for the header. That said, I would prefer to use tags to manage these objects but I'm not able to figure out why the viewWithTag approach is not working.

Side note: I was able to put the UISwitches into an NSMutableArray at run time and manage them that way, but I'd rather not be so verbose, manage bounds violations/indexes on the array or check for nil lists, etc... It is also not clear to me how I would do this using IBOutlets. This is why I'm trying the tag approach.

My goal is to use the switches to collapse / expand the rows in each of the sections which is why I've thought to tag and add UISwitches as sub views to the UIView that I return in viewForHeaderInSection. Then re-reference them later when I need to perform some logic to collapse the cells. Additionally, at run time I could have 1 or more sections so hard-coding the tag numbers is impractical. Here's the code for that method:

Assuming:

#define kSwitchTagRange 2000
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{    
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width, 44)];

    // Add UISwitches and ensure that UISwitch doesn't already exist for this section...
    UISwitch *switchView = (UISwitch*)[self.tableView viewWithTag:(kLayerSwitchTagRange + section)];

    if (switchView == nil) {

        // Create switch
        switchView = [[UISwitch alloc] init];
        double xOffset = self.tableView.bounds.size.width - switchView.frame.size.width;
        switchView.frame = CGRectMake(xOffset,
                                      7,
                                      switchView.frame.size.width,
                                      switchView.frame.size.height);

        [switchView addTarget:self action:@selector(switchChanged:) forControlEvents:UIControlEventValueChanged];

        // Should we tag the switch view?
        switchView.tag = kSwitchTagRange + section;
    }

    // Add switch as subview
    [view addSubview:switchView];    

    return view;
}

In switchChanged: I just reload the table's data:

- (void)switchChanged:(id)sender {

     [self.tableView reloadData];
}

Finally as the table's data is recreated I attempt to retrieve the UISwitch and determine it's on/off state, then I return 0 for the number of rows in the section if OFF and some other number if ON:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    NSInteger rowCount = 0;

    UISwitch *switchView = (UISwitch*)[self.view viewWithTag:(kSwitchTagRange + section)];

    if (switchView != nil && switchView.isOn == NO)
    {
        rowCount = 0;

    }
    else
    {
        // Row count is something other than 0
        rowCount = 3;
    }

    return rowCount;
}

Unfortunately, this switchView is always nil.

I have a few guesses, but can't determine why this is happening.

  • Guess 1: The UIView to which the switch I want was added is deallocated when the table's data was reloaded. It doesn't exist in memory and therefore can't be found searching by tag.
  • Guess 2: I'm adding the UISwitch to the view object incorrectly (in many examples I've seen objects added to a UITableViewCell's contentView, however since I'm sending back a UIView in the viewForHeaderInSection: method, I'm not sure those examples apply here. addSubview should add any object to the tree.

Can anyone tell me why the viewWithTag method above would return nil? I'm leaning toward Guess #1 but haven't found any documentation that tells me the custom header UIView is deallocated at any time. The cells are re-used, but what about the section headers?

Lastly, I've read this post and while the recommendation on tag-use makes sense it seems to dislike the tag approach at all. Why not use tags if you don't find using them to be messy and they're being used intelligently? Why is the tag feature even available?

Really, I just want to know why the viewWithTag method returns nil in my case.

Cheers!

Community
  • 1
  • 1
Aaron
  • 7,055
  • 2
  • 38
  • 53

2 Answers2

1

I'm not sure why your viewWithTag always returns nil, I do think it might have to do with the fact that the views are deallocated at some point.

However, the tags can still work to do what you want, but you need to have a property or key in your model object that keeps track of the value of the switch. You can use the tag to tell which section the switch is in in its action method, and update the model appropriately. Here's what I did. My model is an array of dictionaries where the value of the key "rowData" is an array with all the data for that section, and the value of the key "switchValue" is an NSNumber,0 or 1, representing the state of the switch. So, my data looks like this:

2012-11-28 22:50:03.104 TableWithSwitchesInHeaderView[3592:c07] (
        {
        rowData =         (
            One,
            Two,
            Three,
            Four
        );
        switchValue = 1;
    },
        {
        rowData =         (
            Red,
            Orange,
            Yellow,
            Green,
            Blue,
            Violet
        );
        switchValue = 1;
    },
)

This is what I have in the relevant table view delegate and data source methods:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.theData.count;
}

-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width, 44)];
    UISwitch *switchView = [[UISwitch alloc] init];
    double xOffset = self.tableView.bounds.size.width - switchView.frame.size.width;
    switchView.frame = CGRectMake(xOffset,7,switchView.frame.size.width,switchView.frame.size.height);
    [switchView addTarget:self action:@selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
    switchView.tag = kSwitchTagRange + section;
    switchView.on = [[[self.theData objectAtIndex:switchView.tag - 101] valueForKey:@"switchValue"] boolValue];
    [view addSubview:switchView];
    return view;
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 50;
}

- (void)switchChanged:(UISwitch *)sender {
    NSMutableDictionary *dict = [self.theData objectAtIndex:sender.tag - 101];
    [dict setValue:[NSNumber numberWithBool:sender.isOn] forKey:@"switchValue"];
    [self.tableView reloadData];
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    BOOL switchValue = [[self.theData[section] valueForKey:@"switchValue"] boolValue];
    if (switchValue) {
        return [[self.theData[section] valueForKey:@"rowData"] count];
    }else{
        return 0;
    }

}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    cell.textLabel.text = [[self.theData[indexPath.section]valueForKey:@"rowData"]objectAtIndex:indexPath.row] ;
    return cell;
}

My kSwitchTagRange was #defined as 101. This code worked to collapse the rows (actually get rid of all of them) in any section where the switch was off.

rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • thanks for the response. This is actually an approach that I prefer coming from the Microsoft stack ("binding UI control's properties to corresponding data model object's values"). Given that it's likely that the UISwitch is no longer available to reference, I'll probably go with this approach, but before I do I have a philosophical programming question for you, though: What do you think of the opinion stated [here](http://doing-it-wrong.mikeweller.com/2012/08/youre-doing-it-wrong-4-uiview.html)? – Aaron Nov 29 '12 at 16:42
  • (continued .... ) It recommends tracking the data model object index on the UISwitch (in our case) rather than using a tag. What do you think of this approach? Overkill? Don't care? "Just get it done?" :-) I'm also trying to fully understand the heart of the .tag property and it's use. – Aaron Nov 29 '12 at 16:44
  • 1
    I think I'm in the "it's overkill" camp on this one. I'm not sure how some of his approaches would work when dealing with views in a table view that are constantly being created and deallocated on scrolling. I would prefer that there was some method akin to indexPathForSelectedRow, something like sectionForSelectedView, for header and footer views, but unless I'm missing something, there's not, so we're stuck with something messier. – rdelmar Nov 29 '12 at 17:26
  • I think I'm with you, there. Anyway, I'll assume for now (see second answer below), that the UIView used by the table in viewForHeaderInSection gets deallocated (and thus it's subviews, too) and so have no choice but to use this data-bound approach. I'll consider the question answered. Thanks for the help! – Aaron Nov 29 '12 at 18:00
0

It seems from the post here that the UIView returned by viewForHeaderInSection is deallocated and must be recreated (as well as all it's subviews, including my UISwitch), rather than re-used. I trust that Apple assumes that you'll have far fewer section headers than you are cells and therefore didn't provide a reusability mechanism for retrieving section headers. The data-bound approach proposed by @rdelmar seems to make sense for this scenario.

Community
  • 1
  • 1
Aaron
  • 7,055
  • 2
  • 38
  • 53
  • Here is [another related S.O. thread](http://stackoverflow.com/questions/7132613/viewforheaderinsection-creating-a-memory-leak), which suggests that you manually manage the header's subviews rather than recreate them over and over. – Aaron Nov 29 '12 at 18:10