Swift update UILabel with dispatch_async not working - ios

Why doesn't this work?
dispatch_async(dispatch_get_main_queue(), {
self.timeStringLabel.text = "\(self.timeStringSelected)"
println(self.timeStringLabel.text)
})
I'm trying to update a label in Swift but the UI for the label never changes. I keep googling it, but I can't find any responses that don't use dispatch_async. What am I doing wrong?
1st Edit: I was mistaken. I'm not printing the updated text. The text never changes. It always prints out Optional("0") if that helps. The default value is 0 as defined in the Storyboard.
I have tried it with and without dispatch_async without any success. I also tried adding
self.timeStringLabel.setNeedsDisplay()
Immediately after updating the text, but that also doesn't work.
Edit 2: Here's the complete function + UILabel declaration
#IBOutlet weak var timeNumberLabel: UILabel!
#IBAction func timeNumberButtonPressed(sender: UIButton) {
println("Number Selected. Tag \(sender.tag)")
dispatch_async(dispatch_get_main_queue()) {
self.timeNumberOneButton.selected = false
self.timeNumberTwoButton.selected = false
self.timeNumberThreeButton.selected = false
self.timeNumberFourButton.selected = false
if sender.tag == 0{
self.timeNumberSelected = 0
} else if sender.tag == 1 {
self.timeNumberSelected == 5
} else if sender.tag == 2 {
self.timeNumberSelected == 10
} else {
self.timeNumberSelected == 24
}
sender.selected = true
self.timeNumberLabel.text = "\(self.timeNumberSelected)"
self.timeNumberLabel.setNeedsDisplay()
println(self.timeNumberLabel.text)
}
}
The label is clearly visible as shown in this picture. I didn't think it would be this hard to implement, but I was very wrong. I'm willing to bet it's something really simple that I'm missing.

Try adding the line
self.timeStringLabel.setNeedsDisplay()
(after the label change)
Because the code is run asynchronously, the interface-updating methods may miss the change and not display it (especially if a time-consuming bit of code occurs before the label change). Adding this code forces the interface to check for and display any changes it may have missed.
This should be used after any asynchronous task that changes the interface, as the task's running may overlap with the interface methods, resulting in a missed change.
*Thanks to the iOS Development Journal

Related

UILabel fails to update properly

I am a beginner and working on an iOS app in Xcode (Swift). For some reason I can set the text in a UILabel just fine using its property .text but if I try and update it the label will only show the last value I set (??). For example, here is the code that gets executed when I click a button called myButton and try to set the text in the label called Display. The result is that only the fourth value ("Four") is displayed (after the three 2 second delays) as the UILabel stays blank until that point. The really odd thing is that if I remove everything but the first line (Display.text = "One") it will display it just fine. It is as if the compiler removes all but the last property setting for the label... Any help is appreciated!!
#IBAction func myButton(_ sender: Any)
{
Display.text = "One"
sleep(2)
Display.text = "Two"
sleep(2)
Display.text = "Three"
sleep(2)
Display.text = "Four"
}
Update: This code is just an example that demonstrates a problem I encountered in my real program. The sleep() call is to simulate a delay only. The problem is that I can't seem to set the UILabel's text twice. In my code, I call a function that takes time to execute so I want to display "Wait..." in the label until it is done and then I want to display the result in the same label. But no luck...
Update 2: I found this in Apples documentation: "You provide the content for a label by assigning either a NSString object to the text property, or an NSAttributedString object to the attributedText property. The label displays the most recently set of these properties." Most recent? That seems to be my problem.How can I force it to update on-demand???
Don't use sleep it blocks the main thread
let arr = ["one","two","three","four"]
var counter = 0
Timer.scheduledTimer(withTimeInterval:2.0, repeats:true) { timer in
self.counter += 1
if counter < self.arr.count {
self.display.text = self.arr[counter]
}
else {
timer.invalidate()
}
}
Or use DispatchQueue.main.asyncAfter
arr.indices.forEach {
DispatchQueue.main.asyncAfter(deadline: .now() + Double( $0 * 2) ) {
self.display.text = self.arr[$0]
}
}

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.

Swift - Updating UILabel Prevents Subviews from Changing

I'm currently learning Swift with Stanford's iOS 11 course. One of the projects is a card game called "Set". For that project I have a subview called "cardView" inside the ViewController, where all of the cards on the field are displayed.
Inside that "cardView" I create a subview for each card.
The goal is to find 3 cards that make up a "set".
I also have a hint button, which shows me a possible solution by changing the color of 3 cards that make up a "set".
Everytime I tap on that hint button though my score will decrease.
This is my IBAction:
#IBAction private func hintButtonPressed(_ sender: UIButton) {
for subview in cardView.subviews {
subview.layer.borderWidth = 0
}
game.hint()
if !game.selectedCards.isEmpty {
for index in game.indicesOfSelectedCards {
cardView.subviews[index].layer.borderWidth = 4
cardView.subviews[index].layer.cornerRadius = 8
cardView.subviews[index].layer.borderColor = UIColor.red.cgColor
print("cardView.subview[\(index)] changed borderColor.")
}
game.resetSelectedCards()
} else {
print("There are no sets on the field")
}
scoreLabel.text = "Score: \(game.score)"
}
For some reason if I change the text of the scoreLabel the subviews/cards that make up a "set" won't update and I can't see any change of the border. If I delete this line
scoreLabel.text = "Score: \(game.score)" though I can see the change of the subviews but then I don't see my updated score...
I also worked with breakpoints to figure out on which line this bug occurs and I found out that my subviews won't change as long as I have that line in there where I change the text of the scoreLabel even though that line is at the end of that method.
I just can't figure out how to update the score + show a possible solution at the same time.
Can anyone help me?

Setting UISwitch isOn programmatically from IBAction calls IBAction again

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.

Updating a UILabel Through A Function....Is There Something I'm Missing?

I asked a question similar to this one earlier but this question is more about the general language and fundamentals of Swift. In the code below, shouldn't this function technically work and change the label text? I've been running it for a while now and every time it fails. I've made sure that all of my outlets are linked properly as well. Sorry if the answer is obvious, I'm new to Swift.
func changeLabel() {
DispatchQueue.main.sync(execute: {
self.testText.text = "YES"
})
}
override func viewDidLoad() {
super.viewDidLoad()
let city:String? = nil
if city == nil {
changeLabel()
}
}
viewDidLoad is always called from the main thread (unless a programmer mistakenly does otherwise - and that's a whole other problem).
So there is no point to using DispatchQueue.main.sync to update the label. In fact, it's bad in this case. Calling DispatchQueue.main.sync when already on the main queue will cause the app's user interface to hang until the app is killed.
You have two choices:
Remove the use of DispatchQueue.main.sync since it's not needed in the code you posted.
Change sync to async. This fixes the problem with the app user interface hanging and it also allows you call the changeLabel method from any queue and work properly.
Use this instead:
func changeLabel() {
DispatchQueue.global().async(execute: {
print("teste")
DispatchQueue.main.sync{
self.testText.text = "YES"
}})
Thanks

Resources