Semaphore Wait After Creating UIButton

Hi

In this snippet, I've already added a button, the onDisplay action simply adds another UIButton with an addAction event to the view controller

@IBAction func onDisplay(_ sender: UIButton) {
    let semaphore = DispatchSemaphore(value: 1)
    var didComplete = false

    addButton("hello world") { result in
        print("result: \(result)")
        didComplete = result
        semaphore.signal()
    }

    if semaphore.wait(timeout: .now() + 5) == .timedOut {
        print("timeout: \(didComplete)")
    }

    print("didComplete: \(didComplete)")
}

func addButton(_ message: String, completion: @escaping (Bool) -> Void) {
    let button = UIButton(type: .system) 
    button.frame = CGRect(x: 100, y: 100, width: 100, height: 40)
    button.backgroundColor = .systemBlue
    button.setTitle(message, for: .normal)
    button.titleLabel?.tintColor = .white

    button.addAction(UIAction(handler: { action  in
            completion(true)
    }), for: .touchUpInside)

    view.addSubview(button)
}

What I'm expecting to happen, is the new "hello world" button get created (which it does). If the user does nothing for 5 seconds (semaphore timeout), then print timeout: false.

If the user tapped the button, this would print result: true in the completion closure.

But what happens when I don't touch the button is didComplete: false prints out (the last line) and the semaphore.wait never gets invoked.

How do I make the semaphore execute it's wait operation? Been struggling with this for days 😞

Answered by Frameworks Engineer in 679719022

A better way to do this would be to add a timer that fires 5 seconds in the future but doesn’t block the main thread. Here’s an example:

final class MyViewController: UIViewController {
    var timer: Timer? = nil
    var result: Bool? = nil

    @IBAction onDisplay(_sender: UIButton) {
        addButton("hello world") { result in
            // The button was pressed before the timer expired
            self.result = result
            timer?.invalidate()
            timer = nil
            processButtonResult()
        }

        timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { timer in
            // The timer fired before the button was pressed
            self.timer = nil
            result = false
            processButtonResult()
        }
    }

    func processButtonResult() {
        // Remove the button from the UI
        // ...
        // Use the result
        // ...
    }
}

With creating a semaphore with value 1, wait(timeout:) never waits. If you change it to 0, wait(timeout:) freezes until timeout UI and signal() would not be called until timeout. Conclusion, you should not use semaphore for your purpose.

@OOPer7 that's exactly what's happening! So do you have another suggestion to achieve the experience of waiting to the user to interact with the button before continuing the onDisplay function? I tried using a UIAlertViewController being a model dialog but couldn't find a way to make the dialog appear - probably the same issue with DispatchSemaphore(value: 0). Many thanks!

Never call wait is one clear principle in iOS programming (or in many other GUI programming). If you understand you cannot use DispatchSemaphore here, you may know what to use.

I tried using a UIAlertViewController being a [modal] dialog

My understanding is that you’re trying to present an alert and then block the main thread waiting for the user to respond to that alert. If so, there’s no supported way to do this with UIKit. Blocking the main thread in a Dispatch semaphore would be disastrous — the watchdog can’t distinguish this between this and the app hanging and would just kill your process — but there are other approaches that might seem more feasible but will still cause problems.

You’ll need to restructure your UI code so that it doesn’t need to do this. How you do that depends on your specific requirements, but in most cases it involves maintaining state that tracks whether the alert is presented and starting the deferred operation when the user dismisses the alert.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

A better way to do this would be to add a timer that fires 5 seconds in the future but doesn’t block the main thread. Here’s an example:

final class MyViewController: UIViewController {
    var timer: Timer? = nil
    var result: Bool? = nil

    @IBAction onDisplay(_sender: UIButton) {
        addButton("hello world") { result in
            // The button was pressed before the timer expired
            self.result = result
            timer?.invalidate()
            timer = nil
            processButtonResult()
        }

        timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { timer in
            // The timer fired before the button was pressed
            self.timer = nil
            result = false
            processButtonResult()
        }
    }

    func processButtonResult() {
        // Remove the button from the UI
        // ...
        // Use the result
        // ...
    }
}
Semaphore Wait After Creating UIButton
 
 
Q