daspianist
daspianist

Reputation: 5505

How to break out of a loop that uses async DispatchQueue inside

I am using a for loop coupled with a DispatchQueue with async to incrementally increase playback volume over the course of a 5 or 10-minute duration.

How I am currently implementing it is:

    for i in (0...(numberOfSecondsToFadeOut*timesChangePerSecond)) {
        DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)/Double(timesChangePerSecond)) {
            if self.activityHasEnded {
                NSLog("Activity has ended") //This will keep on printing
            } else {
                let volumeSetTo = originalVolume - (reductionAmount)*Float(i)
                self.setVolume(volumeSetTo)
            }
        }

        if self.activityHasEnded {
            break
        }
    }

My goal is to have activityHasEnded to act as the breaker. The issue as noted in the comment is that despite using break, the NSLog will keep on printing over every period. What would be the better way to fully break out of this for loop that uses DispatchQueue.main.asyncAfter?

Updated: As noted by Rob, it makes more sense to use a Timer. Here is what I did:

    self.fadeOutTimer = Timer.scheduledTimer(withTimeInterval: timerFrequency, repeats: true) { (timer) in
        let currentVolume = self.getCurrentVolume()
        if currentVolume > destinationVolume {
            let volumeSetTo = currentVolume - reductionAmount
            self.setVolume(volumeSetTo)
            print ("Lowered volume to \(volumeSetTo)")
        }
    }

When the timer is no longer needed, I call self.fadeOutTimer?.invalidate()

Upvotes: 1

Views: 883

Answers (2)

Rob
Rob

Reputation: 437842

You don’t want to use asyncAfter: While you could use DispatchWorkItem rendition (which is cancelable), you will end up with a mess trying to keep track of all of the individual work items. Worse, a series of individually dispatch items are going to be subject to “timer coalescing”, where latter tasks will start to clump together, no longer firing off at the desired interval.

The simple solution is to use a repeating Timer, which avoids coalescing and is easily invalidated when you want to stop it.

Upvotes: 3

David Pasztor
David Pasztor

Reputation: 54745

You can utilise DispatchWorkItem, which can be dispatch to a DispatchQueue asynchronously and can also be cancelled even after it was dispatched.

for i in (0...(numberOfSecondsToFadeOut*timesChangePerSecond)) {
    let work = DispatchWorkItem {
        if self.activityHasEnded {
            NSLog("Activity has ended") //This will keep on printing
        } else {
            let volumeSetTo = originalVolume - (reductionAmount)*Float(i)
            self.setVolume(volumeSetTo)
        }
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)/Double(timesChangePerSecond), execute: work)
    if self.activityHasEnded {
        work.cancel() // cancel the async work
        break // exit the loop
    }
}

Upvotes: 1

Related Questions