4

I just noticed that setting a UISwitch's isOn in its IBAction causes the IBAction to be called again. So the following code:

class ViewController: UIViewController {
    var count = 0
    @IBOutlet weak var mySwitch: UISwitch!

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        mySwitch.isOn = false
    }

    @IBAction func buttonTapped(_ sender: UIButton) {
        mySwitch.isOn = !mySwitch.isOn
    }

    @IBAction func switchChanged(_ sender: UISwitch) {
        print("\(count) pre: \(mySwitch.isOn)")
        mySwitch.isOn = !mySwitch.isOn
        print("\(count) post: \(mySwitch.isOn)")
        count += 1
    }
}

prints the following when the switch is turned on one time:

0 pre: true
0 post: false
1 pre: false
1 post: true
  1. switch is turned off in viewDidLoad
  2. switch is turned on by the user
  3. switch is on now when switchChanged (IBAction) is called
  4. 0 pre: true is printed
  5. switch is turned off programmatically in switchChanged
  6. 0 post: false is printed
  7. switchChanged is called again by the system
  8. switch is off now in switchChanged, and 1 pre: false is called
  9. switch is turned on programmatically
  10. 1 post: true is printed

Why is the IBAction called by the system a second time? How does one get around this, say, for example, when wanting to negate the user's action based upon some internal state? I feel like I am missing something embarrassingly obvious, but I'm pretty sure similar code used to work. Is this an iOS bug? It's being run on an iOS 10.2 iPhone 5s simulator, Xcode Version 8.2.1 (8C1002)

It's interesting to note that when the button tied to buttonTapped is tapped (calling that same method), the switch's IBAction is not called.

  • You may have hooked up the switch event handler to two different events. – gnasher729 Mar 03 '17 at 22:23
  • I tested it and saw the same behavior you did. Only if you change the switch by tapping it though, changing it by dragging it only resulted in the action being called once. – dan Mar 03 '17 at 22:26
  • It doesn't make sense to toggle the switch in `switchChanged`. If you want the switch to be off, set it explicitly to off, then it won't matter if the method is called again, since the second time it will be off and you will set it to off and it won't be called again. – Paulw11 Mar 03 '17 at 23:05
  • gnasher729, I did consider that more than one event could be hooked up to the switch, but that's not the case. dan, I confirmed what you saw when dragging the switch. I.e., by dragging to change the switch, the IBAction is called once and the "undo" works like I expected it to initially. – Robert Hartman Mar 04 '17 at 12:22
  • Paulw, maybe that would work out if I just wanted it to be off, but what if I really do want to toggle it back to the initial state, and the question still stands as to why the method is called twice. Further, if setting isOn programmatically results in the method being invoked once, why doesn't it keep getting invoked? – Robert Hartman Mar 04 '17 at 12:28

2 Answers2

0

Your IBAction is presumably hooked up to valueChanged, which doesn't indicate a particular touch event, just exactly what it says, that the value was changed.

I'd suggest setting a variable called something like var didOverrideSwitchValue = false, set it to true just before setting the new switch value, then when the function is called, check for that variable. If it's set to true, then set it to false and return.

Or, if you wish to negate the new setting only when it's turned on, then you could do if (switch.isOn), and then if so then you can respond to it by turning it off, if required.

Andrew
  • 7,693
  • 11
  • 43
  • 81
  • Andrew, that's correct, it's hooked up to `valueChanged`. I'm not sure I completely understand your first suggestion, and let's assume I always want to toggle the boolean. I'm still trying to understand why the set-isOn/IBAction-invocation cycle doesn't continue indefinitely, why dragging the switch behaves differently than tapping, and why the switch's IBAction doesn't get called when `isOn` is changed in `buttonTapped`. – Robert Hartman Mar 04 '17 at 12:39
0

I've been battling the same issue and found a workaround...

Check the "selected" property on the sender in your switch handler. I've found that it's true the first time through and false the second time, so you can tell if you're really being called by the user action.

I'm guessing whatever is teeing up the event to fire the second time isn't the switch itself, or maybe this property gets cleared after the first event is handled. Maybe a UIKit guru could chime in.

The UISwitch docs for -setOn:animated: say

Setting the switch to either position does not result in an action message being sent.

Seems clear enough. Feels like an OS bug.

Anyway, this seems to work but it makes me uneasy because I don't fully understand why the problem exists in the first place, nor exactly why this fixes it, and I worry that either could change in a future OS update.

UPDATE

This works fine in my little test app but not in my real app, which has a more complex UI hierarchy with a nav bar, tabs, etc. This just reinforces my uneasiness with this solution.

Eric McNeill
  • 1,784
  • 1
  • 18
  • 29