Swift code executing before display updates - ios

So I've just started trying out coding in swift and I'm creating an extremely basic app (purely to experiment and learn) where you click a button and two playing cards appear on the screen.
I'm trying to get it so that when the two playing cards are the same, the button to play again disables, the program pauses for a few seconds, then the button re-enables (so later I can add some 'win' text during the pause time).
Now the button and pausing fully works besides one problem. When testing, the program pauses and then when it finishes pausing the display then updates to show the two cards being equal. But while it pauses, it shows two random non equal cards.
I'm not sure why, seeing as the cards update before I check if they're equal, but I'm new to swift (literally last few days) so not sure how it works.
Any ideas? :)
#IBAction func playRoundTapped(sender: UIButton) {
// Change the card image each time the play button is pressed using a random number generator
self.firstCardImageView.image = UIImage(named: String(format: "card%i", arc4random_uniform(13)+1))
self.secondCardImageView.image = UIImage(named: String(format: "card%i", arc4random_uniform(13)+1))
// Check if the cards are equal
if firstCardImageView.image == secondCardImageView.image && firstCardImageView.image != "card" {
playRoundButton.userInteractionEnabled=false;
NSThread.sleepForTimeInterval(4)
playRoundButton.userInteractionEnabled=true;
}
}

Don't sleep in the main thread as this will stop all interactions with your app. You need to replace:
NSThread.sleepForTimeInterval(4)
playRoundButton.userInteractionEnabled=true;
with:
let enableTime = dispatch_time(DISPATCH_TIME_NOW, Int64(4 * Double(NSEC_PER_SEC)))
dispatch_after(enableTime, dispatch_get_main_queue()) {
playRoundButton.userInteractionEnabled=true;
}

First off, a better solution is not pausing at all and using dispatch_after to change the playRoundButton button state after 4 seconds.
If you want to stick with pausing, then you should give time for the UI to update itself before pausing. E.g.,
dispatch_async(dispatch_get_main_thread(), {
//Check if both cards are equal
if firstCardImageView.image == secondCardImageView.image && firstCardImageView.image != "card" {
playRoundButton.userInteractionEnabled=false;
NSThread.sleepForTimeInterval(4)
playRoundButton.userInteractionEnabled=true;
}
});
The fact is, when you assign a new image to your buttons, the button is actually redrawn on screen only at the next run-loop cycle, so if you pause before that is run, no visual change can be seen...
Keep in mind that pausing on the main thread will make your app unresponsive during that timeframe.

You're comparing two UIImage instances, which doesn't work with == because it will only compare the pointers. In your case, it would be much easier to compare the numbers that generated those images.
Other than that, you're pausing the main thread, which takes care of updating the user interface, so it doesn't actually get a chance to do so. One way to solve this problem is by using NSTimer.
#IBAction func playRoundTapped(sender: UIButton) {
let firstNumber = arc4random_uniform(13) + 1
let secondNumber = arc4random_uniform(13) + 1
firstCardImageView.image = UIImage(named: "card\(firstNumber)")
secondCardImageView.image = UIImage(named: "card\(secondNumber)")
if firstNumber == secondNumber {
playRoundButton.userInteractionEnabled = false;
NSTimer.scheduledTimerWithTimeInterval(4.0, target: self, selector: "enableButton", userInfo: nil, repeats: false)
}
}
func enableButton() {
playRoundButton.userInteractionEnabled = true;
}

Related

How do I display a spinner in the middle of UI work?

I have the following function on iOS that performs UI work on a TableView, and the challenge is that if it begins to take longer then 0.5sec. a spinner should be displayed to the user so that the screen doesn't look like it froze.
func updateForm(with rowItems: [RowItem]) {
self.tableView.beginUpdates() // performance tweak.
let viewControllerName = String.init(describing: self.classForCoder) // id
var defaultSection = Form.createSectionWith(tag: viewControllerName, in: form)
// MARK: - update rows
let allRows = self.form.allRows
let startTime = CFAbsoluteTimeGetCurrent()
var showSpinner = false
for (index, item) in rowItems.enumerated() {
.
.
.
<TableView processing work on 100's of rows>
.
.
.
// Evaluate our running time for this process loop, and display spinner if we're past a threshold of seconds.
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
if timeElapsed > 0.5 && showSpinner == false {
showSpinner = true
self.showActivityIndicator(withStatus: "processing") // {NEVER GETS DISPLAYED}
}
} // for (index, item) ...
Of course, when I make the call to showActivityIndicator, it never actually gets displayed.
How can I interrupt and pause the UI work to have the showActivityIndicator spinner animation show-up, then let the loop continue?
You need to get off the main thread in order to allow the run loop to cycle and the redraw moment to occur. A simple delay after showing the spinner before proceeding with your work will solve the problem. But you will have to reorganize your code to allow for that. The easiest solution is to give up your 0.5 second idea and just show the spinner before starting in the first place.
I think there are some more important architectural questions that you need to be addressing with that code, but as a solution to the question asked...
before you start processing the data, create a timer with a delay that will load the spinner when it fires
let delayInSeconds = 5.0
let timer = Timer.scheduledTimer(withTimeInterval: delayInSeconds, repeats: false){
//closure that loads the spinner view on top of the current view
}
then when the processing activity is complete
timer?.invalidate() //cancels the timer if it hasn't fired
and then unload the spinner view

Prevent users from pressing a button x times per second

I have a UIButton within an app that will allow the user to send a request to an API.
How can I prevent the user from pressing this button more than X times per second?
I assume you want to just ignore taps on the button if they are too frequent, but you don't need to set the button to appear disabled while taps are ignored. (If you want to change the button appearance, you'll need to use an NSTimer or other delayed action to re-enable the button, which is a significant complication.)
If you change the requirement to “at least 1/X seconds between taps”, it's a little simpler. For example, instead of allowing at most 4 taps per second, we'll ignore a tap if it comes less than 1/4 seconds after the prior tap.
To implement this requirement, store the time of the last tap. When a tap arrives, see if 1/X seconds have elapsed since the last tap. If not, ignore the tap.
private let minimumTapInterval = 1 / CFTimeInterval(4)
private var lastTapTime = CFAbsoluteTime(0)
#IBAction func buttonWasTapped(sender: AnyObject?) {
let now = CFAbsoluteTimeGetCurrent()
guard now >= lastTapTime + minimumTapInterval else { return }
lastTapTime = now
sendAPIRequest()
}
If you really want to implement a “no more than X taps per second” requirement, you can store the times of the accepted taps. When a new tap comes in, throw away any stored times older than one second. If there are still at least X stored times, ignore the new tap.
private let maxTapsPerSecond = 4
private var tapTimes = [CFAbsoluteTime]()
#IBAction func buttonWasTapped(sender: AnyObject?) {
let now = CFAbsoluteTimeGetCurrent()
let oneSecondAgo = now - 1
tapTimes = tapTimes.filter { $0 >= oneSecondAgo }
// All elements of tapTimes are now within the last second.
guard tapTimes.count < maxTapsPerSecond else { return }
tapTimes.append(now)
sendAPIRequest
}
You can use the isEnabled property of your UIButton.
For the target of your button, add the following at the end of your #objc selector function:
#objc func handleMyButtonPress() {
self.someOtherFunction()
myButton.isEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
myButton.isEnabled = true
}
}
The .now() + 1 deadline is basically saying, after 1 second the button will become enabled again and your users can interact with it.
For a 5 minute delay, you could use .now() + (60 * 5)
I use this myself and it works reliably in almost any situation.
Hopefully, this answer can help you or others with the same problem.

How to make speech synthesiser speak a bunch of utterances, one after another?

I am building an app that will speak the sentences/words that is stored in a playlist one by one. Both the playlists and the items in them are stored inside Core Data.
I added a play/stop button in the storyboard. Here is the event handler for it:
#IBAction func playClick(sender: UIBarButtonItem) {
if isPlaying {
synthesizer.stopSpeakingAtBoundary(.Immediate)
isPlaying = false
sender.image = UIImage(named: "play")
} else {
sender.image = UIImage(named: "stop")
isPlaying = true
for string in playlist.utterrances! {
let utterance = UserSettings.getPrefUtterance((string as! Utterance).string!)
utterance.postUtteranceDelay = 2
synthesizer.speakUtterance(utterance)
}
isPlaying = false
sender.image = UIImage(named: "play")
}
}
isPlaying (type Bool), synthesizer (type AVSpeechSynthesizer) and playlist (type Playlists, an entity description) are class-level variables.
I think the code here is pretty clear. First I decide whether the button should stop or start the synthesizer. If it is the latter, loop through the items in the playlist and synthesise them. Then switch the images and mutate isPlaying
But when I run the app and press the button, nothing at all happens. No sound, no image change, no nothing. I think this is because of the last two lines in the method, so I deleted them. This time, the image of the button changes but still no sound.
I think that this must be because I am telling the synthesiser to start speaking before the last utterance is finished. Or I need to know when the synthesiser stops speaking and start another utterance.
"But why don't you just join the strings and synthesise it just for once?", you asked. Because I want to have a two-second delay between each item in the playlist!
Question:
How can I play the contents of a playlist one after another? (with a noticeable amount of delay in between)
Or
What characters do I need to use to create the AVSpeechUtterance so that it can pause for a noticeable amount of time?
The best way to do this is using the AVSpeechSynthesizerDelegate.
It has a method... speechSynthesizer(_:didFinishSpeechUtterance:) which is called when an utterance finishes.
You can keep track of the utterance that is currently being spoken and when the delegate gets informed of it being finished then start the next one.
If you want them to only play when the button is clicked then disable the button when the delegate gets informed of didStartSpeechUtterance and then enable the button on didFinish....

Swift app development (game w/ SpriteKit) I want some visual changes to appear on the screen and then disappear after a given period of time

I am making a game using Swift and SpriteKit. I have written something of this sort:
node.alpha = 0
sleep(1)
node.alpha = 1
where node is an SKSpriteNode. Instead of making node invisible, freezing the running of the program for one second and making node visible again, what this code does is it simply freezes the running of the program for one second. I figured that all visual changes take place periodically, maybe after each update. How can resolve this and make the node disappear for one second, having the program frozen?
Thank you!
This answer is going to assume you want to just pause the scene, and not the entire program.
What you want to use is the SKActions sequence and waitForDuration,
and the nodes pause variable.
You essentially want to move the entire scene into a separate node, and let another node control the pausing of it.
Set your scene nodes like this:
let sceneWorld = SKNode() //make this available to the entire class
let timerNode = SKNode()
...
override func didMoveToView()
{
....
scene.addNode(timerNode)
scene.addNode(sceneWorld) //sceneWorld is where you will be adding all gfx sprites now
}
func pauseWorld()
{
let wait1sec = SKAction.waitForDuration(1)
let unpause = SKAction.runBlock({sceneWorld.pause = false;showNodes(true)})
timerNode.runAction(SKAction.sequence[wait1sec,unpause])
showNodes(false)
sceneWorld.pause = true;
}
func showNodes(show : Boolean)
{
let alpha = (show) : 1.0 ? 0.0
//set all nodes that need to hide here with alpha
...
}
The solution of luk2303 has worked perfectly for me in my apps.
you can create a timer like this:
node.alpha = 0
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "makeNodeVisible", userInfo: nil, repeats: false)
func makeNodeVisible(){
node.alpha = 1
}
For some reason using the sleep function won't work in SpriteKit because it is meant for the ViewController or something like that.

Swift - slowing down "too fast" animation (UIImage updates) -- aka is NSTimer the only option here?

Newbie to IOS programming - learning through Swift. I'm writing a simple "slot machine / dice game".
I'm trying to show the user a flashing sequence of rolls before the "actual" roll appears.
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
}
}
Die1, etc., are defined as generic UIImage views.
In any case, I'm not seeing the 100x iterations, just the images at the end of the loop. I'm assuming that either it redraws "too fast" or that IOS is trying to be smart, and only draws the last images so as to conserve resources.
I'll wildly guess that I need to either implement some kind of delay here, or, IOS needs to be told to explicitly draw out my images, and not try to outthink my intent.
For the delay, I've seen something about NSTimer, but nothing I saw seems to simply say something like "pause for .05" second, and the whole construct was unclear as they were ObjC examples/conversions.
(Note: I've simplified things here --- normally, I would store the value of RollOne() so I can use it later. I also would like to make an array (or collection?) like Die[1].image, but that is another question.)
========== Edit =======
OK, so I'm following up with more of my original code, merged in with that of #doctordoder so we can discuss a bit better. Hopefully that is kosher. (If this appended question is too long, please advise me on the best way to post a lengthy follow-up directly.)
import UIKit
class ViewController: UIViewController {
//( stripping out stuff unneeded for discussion )
// refers to same label below - works but kosher??
#IBOutlet var btnRoll_x: UIView
#IBAction func btnRoll(sender: AnyObject) {
triggerRoll()
}
var timer : NSTimer? = nil
var rolls : Int = 0
func triggerRoll() {
//hiding is bad UI, but until i know how to "disable & dim"
btnRoll_x.hidden = true
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "doFancyDiceRoll", userInfo: nil, repeats: true);
}
func doFancyDiceRoll() {
Die1.image = PipsImg[randomInt(6)]
Die2.image = PipsImg[randomInt(6)]
Die3.image = PipsImg[randomInt(6)]
if (++rolls > 10)
{
timer?.invalidate()
timer = nil
rolls = 0 // DCB added this presumed missing line
btnRoll_x.hidden = false //again, need to do this better
}
}
}
Hopefully, my implementation of the code is what would have been intended. I made some minor adjustments for (hopeful) clarity.
Good news is I have working code. I have just enough understanding to get in place, but I'm fuzzy on some details.
Here is what I (think I) know...
We declare an NSTImer object, and a roll counter at the main level of the class. I note that in my original version, I had the roll counter scoped within the rolling function itself. Took me a while to understand why it could not live in the DiceRoll loop itself, but now I do. I'm going to express it poorly, but since the timer is going to call DiceRoll multiple instances, it needs to live outside the function.
The button btnRoll gets touched, and invokes triggerRoll().
To prevent the user from touching the button while we are in progress, which put us into a state where the roll counter never got to zero, I hide the button. (I'll figure how to properly put in in disabled state later.)
The timer is set. It fires every .1 second (within limits), and is set to repeat. (until .invalidate ). And it "calls" the function doFancyDiceRoll via the selector: attribute.
So, the big change as previously noted is that doFancy..Roll() no longer loops. It excites a single instance up updating the images. It checks the counter, and if we reach the limit, we kill the timer, which stops the timer (invalidate). (And I unhide the button, making it available again.)
So, a few things I am concerned/wondering about: I get the value of timers for other things that need to happen periodically (take health away every second, check a GPS position every 10 seconds, etc.). It's seems a odd construct to force a screen refresh.
Frankly, I would have expected to see see something like this:
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()] // and 2 and 3 of course.....
VIewController.forceRedraw <<=== something like this, or maybe
ViewController.wait(.05) <<== this?? I dunno ;-)
}
}
instead we end up with about 20 extra lines or so. I'd be interested in knowing if there other approaches that could work keeping the loop intact.
Anyway, assuming this is the one true way to go, I guess my followup to this is how do I pass parameters, since this is not a "real" function call. Trying
selector: "doFancyDiceRoll(40)"
was not objected to by the IDE, but failed in execution.
I had exactly same problem back in days, entire loop is finished before the view is refreshed as #doctordoder mentioned. Solved with using NSTimer
var rollCount = 0
let arrayOfImages = ["image01", "image02", "image03"]
var timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("doFancyDiceRoll"), userInfo: nil, repeats: true)
func doFancyDiceRoll() {
if rollCount == 100 {
timer.invalidate
rollCount = 0
{
else {
//get images from array arrayOfImages[rollCount]
rollCount++
}
}
there could be typos, since I have no Xcode right now.
I have basically the same answer as above :(, but I thought I'd post it anyway.
var timer : NSTimer? = nil
var rolls : Int = 0
func doFancyDiceRoll() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "roll", userInfo: nil, repeats: true);
}
func roll() {
println("rolling")
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
if (++rolls > 100)
{
timer?.invalidate()
timer = nil
}
}
Rather than NSTimer and invalidating, you can use dispatch_after to do the work for you.
func rollDice(howManyTimes: Int) {
die1.image = PipsImg[RollOne()]
die2.image = PipsImg[RollOne()]
die3.image = PipsImg[RollOne()]
if howManyTimes > 0 {
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) / 10.0))
dispatch_after(delayTime, dispatch_get_main_queue()) {
self.rollDice(howManyTimes - 1)
}
}
}
This will run the code for the number of times specified, delaying each time by 0.1 seconds. It works like this: First it sets the images on each die, then, if there are more iterations, it does a dispatch_after to call itself with rollDice(howManyTimes - 1)
With this, you don't need to maintain a NSTimer and it is pretty self contained.

Resources