Setting UISwitch isOn programmatically from IBAction calls IBAction again - ios

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
switch is turned off in viewDidLoad
switch is turned on by the user
switch is on now when switchChanged (IBAction) is called
0 pre: true is printed
switch is turned off programmatically in switchChanged
0 post: false is printed
switchChanged is called again by the system
switch is off now in switchChanged, and 1 pre: false is called
switch is turned on programmatically
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.

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.

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.

Related

Question about deleting UIMenuController default menuItems inside WKWebView in Swift

I wanted to use a custom UIMenuController in WKWebView.
First, I wanted to get rid of the default menu (Copy, Look up, Share), but for some reason I don't know, but it hasn't disappeared.
override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
switch action {
case #selector(highlightHandler):
return true
default:
return false
}
}
func enableCustomMenu() {
let memo = UIMenuItem(title: "메모", action: #selector(highlightHandler))
UIMenuController.shared.menuItems = [memo]
UIMenuController.shared.update()
}
#objc func highlightHandler(sender: UIMenuItem) { }
I tried using the code above to remove the default menuItems and add custom menuItems called "메모", but it didn't.
How can I show only the items I want called "메모"?
canPerformAction() cannot reject an option in most cases. It can only tell the system that the class it's being called in is willing to provide the needed function. Returning False just says "I can't do that one", and then the next item in the responder chain is called and eventually something is found that says "Yes, I can do that". Having said that, it seems that I get a different result if I override this function on the item that is the first responder. In that case, False actually seems to disable the command. So if you can implement canPerformAction() on the first-responder, do that. If not...
Basically you have to temporarily break the responder chain. You do that by overriding the UIResponder "next" variable so that it conditionally returns nil when you want the chain broken. You don't want it to leave it broken for long or bad things will happen. Anything that was approved by the FirstResponder or things in the responder chain between First and you will still be approved, but that will stop approval of things after you in the chain.

iOS Combine Framework - Publisher Only Publishes Once and Then Never Again

I am trying to use the iOS 13 Combine framework in conjunction with some UIKit controls. I want to set up a viewcontroller that contains a switch that enables/disables a button whenever the switch is toggled on/off. According to Apple's documentation, UIKit controls have built-in support for Combine publishers, etc. so this should be possible.
I have a viewcontroller that contains a UISwitch and a UIButton, as shown here:
link to screenshot of my viewcontroller
and here is my code:
import Combine
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var mySwitch: UISwitch!
#IBOutlet weak var myButton: UIButton!
var myCancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
mySwitch.isOn = true // Set initial state of switch
myButton.setTitle("Enabled", for: .normal)
myButton.setTitle("Disabled", for: .disabled)
myCancellable = mySwitch.publisher(for: \.isOn)
.subscribe(on: RunLoop.main)
.assign(to: \.isEnabled, on: myButton)
}
}
The above code should (or so I thought) emit the value of the switch's .isOn property, whenever that property changes, and assign the value to the button's .isEnabled property. If it is running the way I would expect, that means that when the switch is toggled ON the button title should read "Enabled" and the button should be enabled. When the UISwitch is toggled OFF, then the button title should read "Disabled" and the button should be disabled.
But it does not behave the way I am expecting. The value from the switch's publisher is only emitted once, when the publisher is first set up inside viewDidLoad(). When tapping on the switch to toggle it on or off, it never emits a value ever again. I can tell it is at least emitting the value once, because if I change the initial state of the switch to either on or off, the button is set to the expected state when the viewcontroller is loaded.
Typically you are supposed to keep a strong reference to the publisher, or else the publisher/subscriber will be terminated immediately, so that's why I am holding a reference with the myCancellable variable. But this does not fix the issue, the values are still not emitted when tapping on the switch.
Does anyone have any ideas on how to fix this? This seems like it should be a simple "Hello World" type of example using Combine, and I don't know what I am missing here.
A common mistake is thinking that UISwitch's isOn property is KVO-compliant. Sadly, it isn't. You cannot use publisher(for:) to observe it.
Create an #IBAction in your ViewController, and connect the switch's Value Changed event to it.

In swift for iOs,I have an if / else block of code, reacting to changes in a UISwitch. How to set the uiswitch back to off in some situations?

In my swift iOS application, I have a simple UISwitch control. I have connected the value changed outlet to my #IBAction. The code looks like this:
#IBAction func userDidSelectVisibiltySwitch(_ sender: Any) {
if self.visibilitySwitch.isOn {
if badCondition {
self.visibilitySwith.setOn(false, animated: false)
return
}
} else { // Strangely, it executes the else (I think because the compiler is evaluating the isOn condition again when it arrives to the else {}
// work to be done if the user has turned off the switch
}
}
I suspect that in this case, as I am turning the switch off before the else is evaluated, the compiler executes the else {} statement because it evaluates the above isOn expression again. But how is that possible, given that I placed a 'return' instruction ? that is really beyond me. A confirmation of my suspect comes from the fact that if I dispatch_async using GCD the 'self.visibilitySwith.setOn(false, animated: false)' statement, it works properly without executing the else {} statement, because the evaluation of the else takes place before the control is turned off by my statement. My code now looks like this, and it works:
#IBAction func userDidSelectVisibiltySwitch(_ sender: Any) {
if self.visibilitySwitch.isOn {
if badCondition {
DispatchQueue.main.async {
self.visibilitySwith.setOn(false, animated: false)
}
return
}
} else { // In this case it is normal, it does not execute the else {}
// work to be done if the user has turned off the switch
}
}
I think that I am missing something important of swift in this case. Any help is greatly appreciated. I have already provided a solution, but I want to understand the problem. Thanks a lot
Rather than accessing the UISwitch via your sender argument, you go directly to what I assume is the IBOutlet value. Instead of that approach, you can access the sender as outlined below:
#IBAction func userDidSelectVisibiltySwitch(_ sender: UISwitch) {
if sender.isOn && badCondition {
sender.setOn(false, animated: false)
} else { // In this case it is normal, it does not execute the else {}
// work to be done if the user has turned off the switch
}
}
The reason your fix is working is likely because of a slight delay introduced by the dispatch call which allows for the IBOutlet value to update its value.
I have also gone ahead and combined your if statement, as the sample you provide does not require a nested check.
UPDATED BASED ON RMADDY'S COMMENT
This being the solution struck me a bit of code smell, and upon further investigation, I was able to reproduce the scenarios described by OP. This was accomplished by setting the action in Storyboard as seen here:
With that setting, I saw the following:
Original code posted by OP would fail
Adding the DispatchQueue as demonstrated by OP would correct the switch after a brief delay
My posted solution would correctly work
Assuming that this is what the OP has done, then the first correction would be to change the event to Value Changed. Then, as stated by rmaddy in the comment, this would succeed regardless of whether you use the argument or the IBOutlet. Based on the original question, my interpretation was that there was an issue of the outlet value and the switch's state in the interface being out of sync.

Change View with NavigationViewController

all this is probably a trivial question, but I have not found a solution to it. I am making an app for Iphone using Swift.
I have a tableview with some strings and if I press a button I want to navigate back to the previous view directly. However, the code after my call
navigationController?.popViewControllerAnimated(true)
is always run, but I want the current activity to stop and go back to the previous view.
The code looks like:
#IBAction func DeletePressed(sender: UIButton) {
let deleteIndices = getIndexToDelete()
navigationController?.popViewControllerAnimated(true)
print("After navigationController")
for index in deleteIndices{
results?.ListResults[yearShownIndex].months[monthShownIndex].day[dayShownIndex].results.removeAtIndex(index)
}
if (results?.ListResults[yearShownIndex].months[monthShownIndex].day[dayShownIndex].results.count == 0){
results?.ListResults[yearShownIndex].months[monthShownIndex].day.removeAtIndex(dayShownIndex)
}
if (results?.ListResults[yearShownIndex].months[monthShownIndex].day.count == 0){
results?.ListResults[yearShownIndex].months.removeAtIndex(monthShownIndex)
}
if (results?.ListResults[yearShownIndex].months.count == 0){
results?.ListResults.removeAtIndex(monthShownIndex)
}
loadView()
}
"After navigationController" is always displayed.
In android you would start a new activity by creating intents to get the desired behaviour, but how does it work on Iphone?
My problem is that I want to be able to go back directly when navigationController.popViewControllerAnimated is called. This is just a toy example to understand how it works so that I can use it in the if-clauses later.
you could simply add a return statement after you pop the viewcontroller:
#IBAction func DeletePressed(sender: UIButton) {
let deleteIndices = getIndexToDelete()
navigationController?.popViewControllerAnimated(true)
return;
[...]
if you don't wants to execute code after "print("After navigationController")" then remove that code
or it is not possible to remove then toggle it when DeletePressed called

Can't switch state of UISwitch

I have to UISwitches in my application. At launch, they're both set to off and the second switch is disabled and should only be enabled when the first switch is aswell, meaning:
#IBAction func switchOneToggled(sender: UISwitch) {
if switchOne.on {
switchTwo.enabled = true
}
else {
switchTWo.enabled = false
}
}
My problem is, that when I enable switchOne, switchTwo gets enabled, but I can't toggle switchTwo's on/off state by touching it.
Try resetting the switches. I had a similar problem and just deleted the switch and replaced it, and giving it the same connections that is had before. It must be a bug inside Xcode, since it should be able to turn on and off without any code.
#IBAction func switchOneToggled(sender: UISwitch) {
switchTwo.enabled = switchOne.on
}
do you maybe have userinteractionenabled set to false for switchTwo?
The problem is that your switchOneToggled is called when the Switch is tapped and the state of the switch is changed. You should check the state of Switch 1 and enable your Switch 2 in your viewDidLoad.

Resources