Sane Settings

Mike Radin, 2015/12/20

Settings

User preferences often involve categorized collections of binary flags. For iOS applications that most-frequently means UITableViews with multiple sections containing UITableViewCells with UISwitches.

CheckTasks screenshot
CheckTasks, now in the AppStore

These cells can all share the same Prototype in the Storyboard except that every instance of the UISwitch will call the same event handler. Creating a prototype cell per settings cell is undesirable, as is creating separate event handlers for every item and then assigning them in -tableView:cellForRowAtIndexPath:. Maybe you could use tags for that too, but that's not really the intended use-case for those. Here is an alternative.

Define some app-wide constants as follows. These can go into a Constants.m with a matching Constants.h.

NSString *const SettingsAutoDeleteCompleted = @"SettingsAutoDeleteCompleted";
NSString *const SettingsWidgetTaskCount = @"SettingsWidgetTaskCount";
NSString *const SettingsAlarmInterval = @"SettingsAlarmInterval";
NSString *const SettingsSyncReminders = @"SettingsSyncReminders";
NSString *const SettingsGenerateNotifications = @"SettingsGenerateNotifications";

NSString *const SettingsRelativeDates = @"SettingsRelativeDates";
NSString *const SettingsRelativeExtendedFilters = @"SettingsRelativeExtendedFilters";
NSString *const SettingsLongTermTasks = @"SettingsLongTermTasks";
NSString *const SettingsTasksTimers = @"SettingsTasksTimers";
NSString *const SettingsTasksDailyLimit = @"SettingsTasksDailyLimit";

NSString *const SettingsTasksMSBandEnabled = @"SettingsTasksMSBandEnabled";
NSString *const SettingsTasksMSBandShortList = @"SettingsTasksMSBandShortList";
NSString *const SettingsTasksMSBandAlerts = @"SettingsTasksMSBandAlerts";

Now in the SettingsViewController.m

@interface SettingsViewController () {
    …
    NSArray *_sectionTitles;
    NSDictionary *_sectionContent;
    …
}

- (void)viewDidLoad {
    …
    _sectionTitles = @[
        NSLocalizedString(@"Settings", @""),
        NSLocalizedString(@"Procrastination Pack", @""),
        NSLocalizedString(@"Microsoft Band", @"")
    ];

    _sectionContent = @{
        NSLocalizedString(@"Settings", @"") : @[
            SettingsAutoDeleteCompleted,
            SettingsWidgetTaskCount,
            SettingsAlarmInterval,
            SettingsSyncReminders,
            SettingsGenerateNotifications],
        NSLocalizedString(@"Procrastination Pack", @"") : @[
            SettingsRelativeDates,
            SettingsRelativeExtendedFilters,
            SettingsLongTermTasks,
            SettingsTasksTimers,
            SettingsTasksDailyLimit],
        NSLocalizedString(@"Microsoft Band", @"") : @[
            SettingsTasksMSBandEnabled,
            SettingsTasksMSBandShortList,
            SettingsTasksMSBandAlerts]
    };
    …
}

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

- (NSInteger)tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger)section {
    if (section == [_sectionTitles indexOfObject: NSLocalizedString(@"Settings", @"")]) {
        return ((NSArray *)[_sectionContent objectForKey: NSLocalizedString(@"Settings", @"")]).count;
    } else if (section == [_sectionTitles indexOfObject: NSLocalizedString(@"Procrastination Pack", @"")]) {
        return ((NSArray *)[_sectionContent objectForKey: NSLocalizedString(@"Procrastination Pack", @"")]).count;
    } else if (section == [_sectionTitles indexOfObject: NSLocalizedString(@"Microsoft Band", @"")]) {
        return ((NSArray *)[_sectionContent objectForKey: NSLocalizedString(@"Microsoft Band", @"")]).count;
    } else {
        return 0;
    }
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
    UITableViewCell *cell = nil;

    if (indexPath.section == [_sectionTitles indexOfObject: NSLocalizedString(@"Settings", @"")]) {
        NSArray *settingsSectionContent = [_sectionContent objectForKey: NSLocalizedString(@"Settings", @"")];

        if (indexPath.row == [settingsSectionContent indexOfObject: SettingsAutoDeleteCompleted]) {
            cell = [settingsTable dequeueReusableCellWithIdentifier: @"SettingsSwitchCellID"];
            ((UILabel *)[cell viewWithTag: 10001]).text = NSLocalizedString(@"Delete tasks on completion", @"");
            [((UILabel *)[cell viewWithTag: 10001]) sizeToFit];
            ((UISwitch *)[cell viewWithTag: 10002]).on = [_model getFlagSettingsItem: SettingsAutoDeleteCompleted];
        } else if (indexPath.row == [settingsSectionContent indexOfObject: SettingsWidgetTaskCount]) {
            cell = [settingsTable dequeueReusableCellWithIdentifier: @"SettingsTextFieldCellID"];
            ((UILabel *)[cell viewWithTag: 10001]).text = NSLocalizedString(@"Number of tasks in widget", @"");
            ((UITextField *)[cell viewWithTag: 10002]).text = [_intFormatter stringFromNumber: (NSNumber *)[_model getSettingsItem: SettingsWidgetTaskCount]];
        } else if (indexPath.row == [settingsSectionContent indexOfObject: SettingsAlarmInterval]) {
            cell = [settingsTable dequeueReusableCellWithIdentifier: @"SettingsTextFieldCellID"];
            ((UILabel *)[cell viewWithTag: 10001]).text = NSLocalizedString(@"Alarm interval", @"");
            ((UITextField *)[cell viewWithTag: 10002]).text = [_intFormatter stringFromNumber: (NSNumber *)[_model getSettingsItem: SettingsAlarmInterval]];
        }
        …
    }
    …
    return cell;
}

- (IBAction)switchChanged: (id)sender {
    if(![sender isKindOfClass: [UISwitch class]]) { return; }

    UISwitch *toggle = (UISwitch *)sender;

    UITableViewCell *cell = (UITableViewCell *)[self findFirstParent: toggle.superview ofType: [UITableViewCell class]];
    if (!cell) { return; }

    NSIndexPath *indexpath = [settingsTable indexPathForCell: cell];
    if (!indexpath) { return; }

    if (indexpath.section == [_sectionTitles indexOfObject: NSLocalizedString(@"Settings", @"")]) {
        NSArray *settingsSectionContent = [_sectionContent objectForKey: NSLocalizedString(@"Settings", @"")];
        [_model saveSettingsItem: [NSNumber numberWithBool: ((UISwitch *)[cell viewWithTag: 10002]).on] forKey: settingsSectionContent[indexpath.row]];
    } else if (indexpath.section == [_sectionTitles indexOfObject: NSLocalizedString(@"Procrastination Pack", @"")]) {
        NSArray *settingsSectionContent = [_sectionContent objectForKey: NSLocalizedString(@"Procrastination Pack", @"")];
        [_model saveSettingsItem: [NSNumber numberWithBool: ((UISwitch *)[cell viewWithTag: 10002]).on] forKey: settingsSectionContent[indexpath.row]];
    } else if (indexpath.section == [_sectionTitles indexOfObject: NSLocalizedString(@"Microsoft Band", @"")]) {
        NSArray *settingsSectionContent = [_sectionContent objectForKey: NSLocalizedString(@"Microsoft Band", @"")];
        [_model saveSettingsItem: [NSNumber numberWithBool: ((UISwitch *)[cell viewWithTag: 10002]).on] forKey: settingsSectionContent[indexpath.row]];
    }
}

The code above makes several assumptions.

Note that it also works fine with non-flag settings like section-0 row-1.