Reputation: 12621
Imagine a screen S. Users arrive at S, look at stuff. There's a button B ...
| |
| B|
| |
| |
When you press B ..
func clickedB() {
blockingSpinner = true
longCalculation()
blockingSpinner = false
showResult()
}
func longCalculation() {
// a few seconds
}
(We want the user to just wait, seeing a modal spinner, if/while the calculation is happening.)
Typically when a user arrives on screen S, they look at something else for a couple of seconds before touching B.
So ...
var waitor = DispatchSemaphore(value: 0) // or ???
func viewDidLoad() {
DispatchQueue.global(qos: .background).async { longCalculation() }
}
func longCalculation() {
something waitor
do the calculation
something waitor
DispatchQueue.main.async {
something waitor
}
}
func clickedB() {
// (note that ... calculation may have finished ages ago
// or we may be in the middle of it, it has a second or so remaining
// or perhaps even this is the second+ time the user has clicked B)
something waitor
if/while longCalculation is still running,
blockingSpinner = true
blockingSpinner = false
showResult()
}
I'm afraid I have no clue how to use DispatchSemaphore
in that scenario.
The particular way they made wait()
and signal()
work don't seem to add up here.
How to use DispatchSemaphore
in this scenario?
Upvotes: 4
Views: 450
Reputation: 64002
You want just one thing to be able to proceed at a time, so your semaphore's value should be 1. Given that, you could also just as easily use an NSLock
. But here's the outline of using the semaphore.
When you start the calculation, wait()
(indefinitely) on the semaphore. As you've suggested, you can take advantage of the view controller lifecycle's inherent ordering to know that this will not actually block. Whenever you finish, `signal(). Obviously this should all be done in the background so that the main thread is not blocked.
When you process the button tap, test the semaphore by "waiting" on it with an instantaneous timeout, and show the spinner if you get .timedOut
as a result. We can do this on the main thread, because whether the semaphore is available or times out, there will be no actual wait time. Note that no signal is needed here: if the waiting times out, the semaphore is re-incremented automatically. If the wait succeeds, then the job is done -- proceed directly to showing the result.
If the job is not done, you're showing the spinner; now wait (indefinitely) in the background. When that wait ends -- because the semaphore was signaled -- hop back to the main thread, dismiss the spinner, and present the results.
At this point, the semaphore's count is at 0, so if you will ever need to go through this code path again you must signal the semaphore.
Basically, filling out the code sketch you made:
class WhateverViewController : UIViewController
{
private let semaphore = DispatchSemaphore(value: 1)
override func viewDidLoad()
{
super.viewDidLoad()
self.performLongCalculation()
}
private func performLongCalculation()
{
DispatchQueue.global(qos: .background).async {
self.semaphore.wait()
// Synchronous processing...
self.semaphore.signal()
}
}
private func buttonTapped()
{
if self.semaphore.isBusy {
self.waitForResult()
}
else {
self.showResult()
}
}
private func buttonTappedAlternative()
{
// Show the spinner unconditionally, if you assume that the
// calculation isn't already done.
self.waitForResult()
}
private func waitForResult()
{
self.showSpinner()
DispatchQueue.global(qos: .userInitiated).async {
self.semaphore.wait()
DispatchQueue.main.async {
self.dismissSpinner()
self.showResult()
}
}
}
private func showResult()
{
// Put stuff on screen
self.semaphore.signal()
}
}
Where buttonTapped
is using a convenience on DispatchSemaphore
extension DispatchSemaphore
{
var isBusy : Bool { return self.wait(timeout: .now()) == .timedOut }
}
And you could reverse this logic if you prefer: isIdle
would just be self.wait(timeout: .now()) == .success
Upvotes: 1