Ad

Our DNA is written in Swift
Jump

Cells with Switch

When I revisited the settings screen on MyAppSales to add a switch it turned my stomach how I would have added it over a year ago versus to the way I’m doing it now.

The Old Way

Consider the following snipped just to get a similar nausea so that you can appreciate what I am going to show you afterwards. That’s from a random switch in cellForRowAtIndexPath.

NSString *CellIdentifier = @"ServerSectionSwitch";
 
SwitchCell *cell = (SwitchCell *)[tableView dequeueReusableCellWithIdentifier:
		CellIdentifier];
if (cell == nil)
{
	cell = [[[SwitchCell alloc] initWithFrame:CGRectZero reuseIdentifier:
		CellIdentifier] autorelease];
}
 
cell.titleLabel.text = @"Enable on WLAN";
ASiSTAppDelegate *appDelegate = (ASiSTAppDelegate *)[[UIApplication sharedApplication]
		delegate];
 
cell.switchCtl.on = appDelegate.serverIsRunning;
[cell.switchCtl addTarget:appDelegate action:@selector(startStopServer:)
			forControlEvents:UIControlEventValueChanged];
return cell;

So obviously I had created a custom tableview cell, so let’s glance at that as well.

SwitchCell.h

#import 
 
 
@interface SwitchCell : UITableViewCell {
	UISwitch *switchCtl;
	UILabel *titleLabel;
 
}
 
@property (nonatomic, retain) UILabel *titleLabel;
@property (nonatomic, retain) UISwitch *switchCtl;
 
@end

SwitchCell.m

#import 
 
 
@interface SwitchCell : UITableViewCell {
	UISwitch *switchCtl;
	UILabel *titleLabel;
 
}
 
@property (nonatomic, retain) UILabel *titleLabel;
@property (nonatomic, retain) UISwitch *switchCtl;
 
@end
 
#import "SwitchCell.h"
 
#define MAIN_FONT_SIZE 16.0
#define LEFT_COLUMN_OFFSET 10.0
 
@implementation SwitchCell
 
@synthesize switchCtl, titleLabel;
 
- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = TABLEVIEWCELL_PLAIN_INIT) {
        // Initialization code
		self.selectionStyle = UITableViewCellSelectionStyleNone;
 
		// layoutSubViews will decide the final frame
		titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
		titleLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters;
		titleLabel.font = [UIFont boldSystemFontOfSize:MAIN_FONT_SIZE];
		[self.contentView addSubview:titleLabel];
 
		switchCtl = [[UISwitch alloc] initWithFrame:CGRectZero];
		[self.contentView addSubview:switchCtl];
    }
    return self;
}
 
 
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
 
    [super setSelected:selected animated:animated];
 
    // Configure the view for the selected state
}
 
- (void)layoutSubviews
{
	[super layoutSubviews];
    CGRect contentRect = [self.contentView bounds];
 
	NSString *text = titleLabel.text;
	CGSize sizeNecessary = [text sizeWithFont:[UIFont systemFontOfSize:
		MAIN_FONT_SIZE]];
 
    CGRect frame = CGRectMake(contentRect.origin.x + LEFT_COLUMN_OFFSET ,
		contentRect.origin.y,  sizeNecessary.width+20.0,  contentRect.size.height);
	titleLabel.frame = frame;
 
	frame = CGRectMake(contentRect.origin.x + contentRect.size.width - 91.0 -
		 LEFT_COLUMN_OFFSET,contentRect.origin.y+
				(contentRect.size.height-27.0)/2.0,
					   94.0, 27.0);
	switchCtl.frame = frame;
}
 
- (void)dealloc
{
	[titleLabel release];
	[switchCtl release];
    [super dealloc];
}
 
@end

Ok, you feel my sickness over this code? Let me explain, these are the reasons why this is crappy coding.

  • It’s completely unnecessary to make a subclass for UITableViewCell just to add a subview, in this case a UISwitch. This adds so much extra code that it’s ridiculous. Unmaintainable. Manual. Laughable.
  • You’ll laugh out loud (LOL) when you see how I am doing it today and how much less code is necessary. this code completely ignores the cell’s accessoryView and instead builds it’s own view hierarchy.
  • Wasn’t it SDK 2.1 when we sitched from initWithFrame to initWithStyle?
  • Obviously I did not know about NSUserDefaults back then, because why else would I save user settings in a “global” variable in my app delegate?
  • Apart from that, I’m calling an action in a different class, makes it harder to understand for other programmers
  • But hey, as I said this code is from about 2 years ago. Thankfully I know now better and here I’m going to share my wisdom. Buckle up!

    The Warp-Speed Way

    First and foremost I don’t need this extra class any more, because we’re going to do it using a method that takes care of sizing and positioning of the switch for us automatically.

    NSString *CellIdentifier = @"ReportSection3";
     
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    UISwitch *switcher;
    if (cell == nil)
    {
    	cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
    				reuseIdentifier:CellIdentifier] autorelease];
    	switcher = [[[UISwitch alloc] initWithFrame:CGRectZero] autorelease];
    	[switcher addTarget:self action:@selector(toggleSumOnOverview:)
    				 forControlEvents:UIControlEventValueChanged];
    	cell.accessoryView = switcher;
    }
    else
    {
    	switcher = (UISwitch *)cell.accessoryView;
    }
     
    // make it multiline
    cell.textLabel.text = @"Fetch royalty sum and\nshow it on overview (slow)";
    cell.textLabel.numberOfLines = 0;
    cell.textLabel.font = [UIFont boldSystemFontOfSize:13];
    cell.textLabel.adjustsFontSizeToFitWidth = YES;
     
    // set from user defaults
    switcher.on = [[NSUserDefaults standardUserDefaults] boolForKey:
    				@"RoyaltyTotalsOnOverView"];
    return cell;

    At the top you see the usual dequeueReusableCellWithIdentifier. Because it’s special cell with the switch being an addition we give it a new identifier. In case there isn’t already such a cell we make a new one, create an autoreleased UISwitch and set it as the cell’s accessoryView.

    On the other hand if we got back a cell from the dequeue we set our switcher variable to the accessoryView which has to be a UISwitch, because we put it there. Then I did some adjusting of the text label to fit the long text. Finally the current switch status of the switch is equal to a value in our user defaults.

    In short we are getting or creating a switch-enabled cell and set the value of the switch. What’s great about this approach is that we don’t have to do anything to position the switch or even find out it’s correct size. All of this is done automatically. UISwitches know their ideal size (sizeThatFits:) and cells know how to position their accessoryView based on it’s size. Actually I am convinced that this is the same method Apple uses for whenever you see a switch or any other accessory for that matter in a tableview cell. Another effect you get for free is that if the cell is put into edit mode any textLabel and detailTextLabel will get resized automatically.

    The second step, because we no longer call complicated functions to store a user setting somewhere in the app delegate, just reacts to whenever the switch’s value changes. The user defaults mechanism automatically takes care of persisting the changed value and as I have shown recently you can also easily specify default values for the defaults.

    - (void)toggleSumOnOverview:(UISwitch *)switcher
    {
    	[[NSUserDefaults standardUserDefaults]
    			setBool:switcher.on forKey:@"RoyaltyTotalsOnOverView"];
    }

    Too simple for you? Well, we still need to react to a change of this user setting. So, in the init of the affected viewController all I need to add is:

    // if defaults setting changes
    [[NSNotificationCenter defaultCenter] addObserver:self
    		selector:@selector(reloadTableView:)
    		name:NSUserDefaultsDidChangeNotification object:nil];

    This calls a method to reload the contents of its table view whenever any value of the user defaults changes. Of course there needs to be a removeObserver in dealloc as well, but you can imagine this.

    Advanced Tactics

    I love the way Apple lets the cell monitor the size of the accessoryView and take care of all necessary resizing automatically. One scenario where this is especially useful is if you have a purchase button like DTPurchaseButton that enlarges to the left if you push it. Say, in neutral state, the button is labeled $0.99 and if you push it once this changes to BUY NOW. I said it before and I’ll say it again: the resizing of the textLabel in a tableview cell is done automatically for you!

    Take a look at the screenshot below. This has a purchase button as accessoryView in a transparent tableview cell. See how the textLabel automatically decreases its size when the button becomes larger?

    This is from a famous Twitter client for which I quite recently implemented In-App Purchasing with my DTShop+DTPurchaseButton killer combo. I will randomly select a winner for a free copy of DTPurchaseButton+DTShop, the easiest way to add IAP to your apps. To enter in the draw, just twitter me the correct answer @dr_touch plus a link to this article before end of July 31st.


    Categories: Recipes

1 Comment »

  1. Dr Touch,
    This is off topic i know,but is it possible to add the tab view shown at the top to be added as a detail view(ie a view which is navigated into from some other view)?I tried but found that although the corresponding views for each tab appear,no further navigation is possible in any of them!