Search code examples
objective-ciosios5

UISwitch does sometimes not send UIControlEvents


I have a model object which includes a boolean flag. I want to show the value of the flag using a UISwitch. The value can be changed in two ways:

  1. First by the user by toggling the switch. I register the UIControlEventTouchUpInside for that. I also tried UIControlEventValueChanged – it has the exact same effect.
  2. Second by some external state change. I use a timer to check for that state change and set the on property of the switch accordingly.

However, setting the value of the switch in the timer method has the effect that the touchUpInside action is sometimes not triggered even though the user has touched the switch.

So I face the following problem: If I set the switch state in the timer when the state changes externally, I loose some state changes from the user. If I don't use the timer I get all state changes from the user. However, I miss all the external state changes, then.

Now I have run out of ideas. How can I achieve what I want, getting both types of state changes in the model and reflected them correctly in the switch view?

Here is a minimal example that shows the problem. I have replaced the model object by a simple boolean flag, and in the timer I don't change the flag at all, I just call setOn:animated:. I count the invocations of the action method. Like that I can easily find out how many touches were missed:

#import "BPAppDelegate.h"
#import "BPViewController.h"

@implementation BPAppDelegate {
    NSTimer *repeatingTimer;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    BPViewController *viewController = [[BPViewController alloc] init];
    self.window.rootViewController = viewController;
    [self.window makeKeyAndVisible];
    [self startTimer];
    return YES;
}

- (void) startTimer {
    repeatingTimer = [NSTimer scheduledTimerWithTimeInterval: 0.2
                                                      target: self.window.rootViewController
                                                    selector: @selector(timerFired:)
                                                    userInfo: nil
                                                     repeats: YES];
}

@end


#import "BPViewController.h"

@implementation BPViewController {
    UISwitch *uiSwitch;
    BOOL value;
    int count;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    value = true;
    uiSwitch = [[UISwitch alloc] init];
    uiSwitch.on = value;
    [uiSwitch addTarget:self action:@selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:uiSwitch];
}

- (void)touchUpInside: (UISwitch *)sender {
    count++;
    value = !value;
    NSLog(@"touchUpInside: value: %d, switch: %d, count: %d", value, sender.isOn, count);
}

- (void) timerFired: (NSTimer*) theTimer {
    NSLog(@"timerFired: value: %d, switch: %d, count: %d", value, uiSwitch.isOn, count);
    // set the value according to some external state. For the example just leave it.
    [uiSwitch setOn:value animated:false];
}

@end

Solution

  • My original code works as expected in iOS 7. So it seems to have been a bug in iOS 6.