Delay in For Loop Swift - swift

I am creating a simple card game (Set) in SwiftUI. I have a button that will deal X new cards when tapped. Currently, it makes all cards show up at once. I was wondering how I could make them come out one at a time.
Deal works by appending a new card to a Deck array in the model. ContentView displays each card in the grid.
This is what I currently have after looking online. Displays first card then next all at once
func deal(_ numberOfCards: Int) {
withAnimation(Animation.easeInOut(duration: 1)) {
viewModel.deal()
}
for _ in 1..<numberOfCards {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
withAnimation(.easeInOut) {
viewModel.deal()
}
}
}
}

Try this
func deal(_ numberOfCards: Int) {
withAnimation(Animation.easeInOut(duration: 1)) {
viewModel.deal()
}
for i in 1..<numberOfCards {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.7) {
withAnimation(.easeInOut) {
viewModel.deal()
}
}
}
}

The problem is that you’re starting all of them 0.7 seconds from now. You want to multiply that interval by the for loop index. You can probably also simplify it a bit, e.g.:
func deal(_ numberOfCards: Int) {
for i in 0..<numberOfCards {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.7) {
withAnimation(.easeInOut) {
viewModel.deal()
}
}
}
}
This pattern isn’t ideal, though, because if you dismiss the view in question, it’s still going to be trying to flip cards on view that isn’t present anymore. Also, this pattern of issuing multiple asyncAfter is not great because it’s subject to timer coalescing (where latter calls may be coalesced together to save battery). This latter issue might not be an issue here, but as a general rule, we use Timer to prevent coalescing and to be able to cancel the timer when the view is dismissed.
func deal(_ numberOfCards: Int) {
var cardNumber = 0
let timer = Timer.scheduledTimer(withTimeInterval: 0.7, repeats: true) { timer in
withAnimation(.easeInOut) {
viewModel.deal()
}
cardNumber += 1
if cardNumber >= numberOfCards {
timer.invalidate()
}
}
timer.fire()
}
If this was in a class, I might use [weak self] in the timer closure with
guard let self = self else {
timer.invalidate()
return
}

Related

comletion for function. Only one tap on button

i am trying to make func completion to use it everywhere. I want to have only one tap on button per second.
i have code like this
var isAllowedToExit = true
if isAllowedToExit {
interactor.exitProfile()
isAllowedToExit = false
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.isAllowedToExit = true
}
it works perfect, but it is a lot of code to use it everywhere
I am trying to do like this
func onlyOneTap(completion: () -> ()) {
var isAvailableToTap: Bool = true
if isAvailableToTap {
isAvailableToTap = false
completion()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
isAvailableToTap = true
}
}
then use it like this
onlyOneTap {
interactor.exitProfile()
}
but it doesnot even work. how can i fix my code, and do i have some variants to make it global like extension to use everywhere?
you can declare the button that you want to use function as outlet and use that function
#IBOutlet weak var someButtonOut: UIButton!
func onlyOneTap(buttonInput:UIButton) {
buttonInput.isEnabled = false
// your completion
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
buttonInput.isEnabled = true
}
}

Is there a way to delay a return-Statement in Swift?

I wondered if there is any way to delay the return statement of an function... an example:
func returnlate() -> String {
var thisStringshouldbereturned = "Wrong"
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Here should be the String changed
thisStringshouldbereturned = "right"
}
// The Return-Statement returns the First Value ("Wrong")
// Is there a way to delay the return Statement?
// Because you can't use 'DispatchQueue.main.asyncAfter(deadline: .now() + 1)'
return thisStringshouldbereturned
}
Thanks, have a nice day and stay healthy.
Boothosh
You are looking for a closure, a.k.a. completion handler.
return executes immediately and there is no way to delay that. Instead, you can use completion handlers, which work by passing in a closure as a parameter. This closure can then be called after a delay.
/// closure here!
func returnLate(completion: #escaping ((String) -> Void)) {
var string = "Wrong"
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
string = "right"
completion(string) /// similar to `return string`
}
}
override func viewDidLoad() {
super.viewDidLoad()
returnLate { string in
print("String is \(string)") /// String is right
}
}

Executing text-to-speech in order

I want to synthesize text. I have an array of sentences and array of pauses, that I wish between these sentences.
What was the thought
Synthesize -> start the timer, timer fires after provided time -> Synthesize -> start the timer -> Synt...
By chance, I've noticed that timer fires the lesser time first, instead of executing and setting up timers in sequence. The loop doesn't wait till synthesizer finished to pronounce, it continues to run.
How to work out that synthesizer pronounces sentences with provided pauses, and in order?
import SwiftUI
struct KingsSpeechView: View {
#ObservedObject var speaker = Speaker()
#State private var subtitles = ""
#State private var currentStepIndex = 0
let kingsSpeech = [
"Hello. Let's start the Game! Let the hunger Games Begin...Whoa-Whoa. Here're are the rules on the screen.",
"Okey, now that you know the rules, chill out. Let's play another game.",
"You say Hi, I say Ho.",
"Hooo",
"Hooo"
]
var pauses = [0.0, 20.0, 90.0, 40.0, 40.0]
// try to change into this
// var pauses = [0.0, 20.0, 10.0, 5.0, 5.0]
// the sequence of execution is completely different
// the ones that has less value, will execute first
// While I expected it to execute in order it is in array, instead it runs as it runs (wants)
// (or maybe it's the case it's just one timer for all)
// How to prevent loop from continuing to new iteration until the speech is not pronounced?
var body: some View {
VStack {
Text(subtitles)
.padding(.bottom, 50)
.padding(.horizontal, 20)
Button("Play") {
playSound()
}
}
}
func playSound() {
for step in 0..<kingsSpeech.count {
let timer = Timer.scheduledTimer(withTimeInterval: pauses[step], repeats: false) { timer in
subtitles = kingsSpeech[step]
speaker.speak("\(kingsSpeech[step])")
print("I am out")
currentStepIndex += 1
// I've tried to stop a loop from moving on, before the speech had finished to pronounce
// with some sort of a condition maybe; by index or by identifying if the synthesizer is speaking
// but it even turned out that timer executes completely different, look in time arrays above
// while speaker.semaphoreIndex == step {
// print("still waiting")
// }
// while speaker.synth.isSpeaking {
//
// }
}
}
}
}
...
import AVFoundation
import Combine
class Speaker: NSObject, ObservableObject, AVSpeechSynthesizerDelegate {
let synth = AVSpeechSynthesizer()
// started to try something with simophore, but didn't understand how to implement it
var semaphore = DispatchSemaphore(value: 0)
var semaphoreIndex = 0
override init() {
super.init()
synth.delegate = self
}
func speak(_ string: String) {
let utterance = AVSpeechUtterance(string: string)
utterance.voice = AVSpeechSynthesisVoice(language: "en-GB")
utterance.rate = 0.4
synth.speak(utterance)
}
}
extension Speaker {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
print("all done")
semaphore.signal()
semaphoreIndex += 1
}
}
Just speak an utterance, receive the delegate method, and in that method wait the desired interval and go on to the next utterance and interval.
Here's a complete example. It uses a Cocoa project, not SwiftUI, but you can easily adapt it.
import UIKit
import AVFoundation
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class Speaker : NSObject, AVSpeechSynthesizerDelegate {
var synth : AVSpeechSynthesizer!
var sentences = [String]()
var intervals = [Double]()
func start(_ sentences: [String], _ intervals: [Double]) {
self.sentences = sentences
self.intervals = intervals
self.synth = AVSpeechSynthesizer()
synth.delegate = self
self.sayOne()
}
func sayOne() {
if let sentence = sentences.first {
sentences.removeFirst()
let utter = AVSpeechUtterance(string: sentence)
self.synth.speak(utter)
}
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
if let interval = intervals.first {
intervals.removeFirst()
delay(interval) {
self.sayOne()
}
}
}
}
class ViewController: UIViewController {
let speaker = Speaker()
override func viewDidLoad() {
super.viewDidLoad()
let sentences = [
"I will speak again in one second",
"I will speak again in five seconds",
"I will speak again in one second",
"Done"]
let intervals = [1.0, 5.0, 1.0]
self.speaker.start(sentences, intervals)
}
}
Trying to answer the question that I asked in comments to solution:
For now, it can play/ pause
TODO: Now I have to discover how to jump backward/ forward between sentences. So, for this I firstly need to stop the current speech task. speaker.synth.stopSpeaking(at: .word)
Then, I maybe should have some index tracking what's the current stage is. Then, when I stopped the task, I remember the index. And I can go backward/ forward. Now start from index-1 or index+1 place, rather than from the beginning.
#State private var isPlaying = false
...
// play button
Button(action: {
if isPlaying {
isPlaying.toggle()
speaker.synth.pauseSpeaking(at: .word)
} else {
isPlaying.toggle()
// continue playing here if it was paused before, else ignite speech utterance
if speaker.synth.isPaused {
speaker.synth.continueSpeaking()
} else {
speaker.speak()
}
}
}, label: {
Image(systemName: (isPlaying ? "pause.fill" : "play.fill"))
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
})

In Swift, how can I guarantee that only 1 task runs/wait at the most of the time?

In Swift, I have a recursive function which I want it to be executed every minute. It looks something like below.
func someFunc() {
// business logics...
DispatchQueue.global().asyncAfter(deadline: .now() + 60) {
self.someFunc()
}
}
The thing is that this someFunc() can be initiated by multiple callers, but I only want to allow one instance of someFunc() running or waiting to be executed in the future.
What is the best way to guarantee that at most 1 someFunc() will be running or queued at any given timeeeee?
(I am using Swift 5.3)
You can also use NSLock for this:
import PlaygroundSupport
let lock = NSLock()
func someFunc(msg: String) {
guard lock.try() else { return }
print(msg)
DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
lock.unlock()
someFunc(msg: msg)
}
}
someFunc(msg: "call1")
someFunc(msg: "call2") // Ignored
someFunc(msg: "call3") // Ignored
PlaygroundPage.current.needsIndefiniteExecution = true
You can use a boolean to keep track of the function call is queued (assuming you're not calling someFunc() from different threads)
var isQueued = false
func someFunc() {
if isQueued { return }
// business logics...
isQueued = true
DispatchQueue.global().asyncAfter(deadline: .now() + 60) {
self.isQueued = false
self.someFunc()
}
}
Example
class Example {
var isQueued = false
func someFunc(_ i: Int) {
if isQueued { return }
// business logics...
if i > 500 {
return
}
print(i)
isQueued = true
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
self.isQueued = false
self.someFunc(i * 10)
}
}
}
let t = Example()
t.someFunc(1)
sleep(2)
t.someFunc(2)
sleep(1)
t.someFunc(3)
sleep(1)
t.someFunc(4)
sleep(1)
t.someFunc(5)
prints
1
10
100
4
40
400

Dispatchqueue and SVProgressHUD in swift

I have been designing an app that analyzes lines of text, and I want to use SVProgressHUD to show progress of it.
This is my code:
let total = text.count
for line in text{
count = count + 1
DispatchQueue.global(pos: .background).async{
//Analyzing line
DispatchQueue.main.async{
SVProgressHUD.showProgress(count/total)
}
}
}
The analyzing works, and the HUD shows properly, when the count reaches total, the process gets stuck, and SVProgressHUD stops at max status, and the program stops. What is the issue with the program?
Am I using Dispatchqueue wrong?
Do I have to call something other to free the background process or something?
I have tried exchanging //Analyzing line and SVProgressHUD.show..., but it still doesn't work. I initially used SVProgress within the loop without the dispatchqueue, but then the progress hud moves only after the analyzation(full loop) has been completed, which is a problem.
Any help would be appreciated.
Thank you.
Try using this code. It does not use loop but implements recursive calls to a function for processing your string data.
func processLines() {
SVProgressHUD.showProgress(0)
// sample data
var text = Array("abcdefghijklmnop")
var lines : [Line] = []
let count : [Int] = Array(0..<text.count)
count.forEach({ pos in lines.append(Line(char: text[pos])) })
var currentIndex : Int = 0
func processLine(at index: Int) {
DispatchQueue.global(qos: .background).async{
//Analyzing line
let line = lines[index]
print("Processing Line CHAR: \(line.char)")
DispatchQueue.main.async{
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
guard currentIndex < lines.count-1 else {
SVProgressHUD.showProgress(1)
return
}
currentIndex += 1
startLineProces(at: currentIndex)
}
}
}
}
func startLineProces(at index: Int) {
processLine(at: index)
SVProgressHUD.showProgress(Float(index) / Float(lines.count))
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
startLineProces(at: currentIndex)
}
}
struct Line {
var char: Character
init(char: Character) {
self.char = char
}
}
Use the below string extension to analyse your string. It has a completion block which will return progress as well as status of completion.
extension String {
func analyseString(completion: #escaping (Bool, Float) -> ()) {
let totalCountOfString = self.count
for (index, _) in self.enumerated() {
if index == totalCountOfString - 1 {
completion(true, Float(index)/Float(totalCountOfString))
} else {
completion(false, Float(index)/Float(totalCountOfString))
}
}
}
}
You can call the above method to show your progress as below (maybe on a button click). self.yourString is your input string that you need to analyse.
#IBAction func clicked(_ sender: UIButton) {
DispatchQueue.main.async {
self.yourString.analyseString { (isCompleted, progress) in
if isCompleted {
SVProgressHUD.dismiss()
print("Ending")
} else {
SVProgressHUD.showProgress(progress, status: "Analysing (\(progress)%)")
}
}
}
}