Search code examples
iosswiftdelegatesclosureschess

How do I make a closure wait until the player pressed a button in a different view controller?


I am writing a chess GUI in Swift 3 and use nvzqz/Sage as the chess model/library. Now I face a problem with a Sage closure used for piece promotion.

Sage uses (in its game class) the execute(move: promotion:) method for promotion move execution which has a closure that returns a promotion piece kind. This allows to prompt the user for a promotion piece or perform any other operations before choosing a promotion piece kind, as follows:

try game.execute(move: move) {
    ...
    return .queen
}

I implemented a promotion view controller ("pvc") which is called in this closure so that the player may select the new piece:

// This is in the main View Controller class

/// The piece selected in promotionViewController into which a pawn shall promote
var newPiece: Piece.Kind = ._queen // default value = Queen

try game.execute(move: move) {
    boardView.isUserInteractionEnabled = false

    // promotionview controller appears to select new promoted piece
    let pvc = PromotionViewController(nibName: nil, bundle: nil)
    pvc.delegate = self
    pvc.modalPresentationStyle = .overCurrentContext
    self.present(pvc, animated:true)

    return newPiece
}

When the button for the new piece in the pvc is pressed, the pvc dismisses itself and the data of the selected piece (the constant selectedType) is transferred back to the main view controller via delegation:

// This is in the sending PVC class
protocol PromotionViewControllerDelegate {
    func processPromotion(selectedType: Piece.Kind)
}

func buttonPressed(sender: UIButton) {
    let selectedType = bla bla bla ...
    delegate?.processPromotion(selectedType: selectedType)
    presentingViewController!.dismiss(animated:true)
}


// This is in the receiving main View Controller class
extension GameViewController: PromotionViewControllerDelegate {

func processPromotion(selectedType: Piece.Kind) {
    defer {
        boardView.isUserInteractionEnabled = true
    }
    newPiece = selectedType
}

The problem I have is that the closure (in the game.execute method) does not wait until the player made his selection in the pvc (and immediately returns the newPiece variable which is still the default value) so that I never get another promotion piece processed other than the default value.

How do I make the closure wait until the player pressed a button in the pvc?

Of course, I tried to find a solution and read about callbacks, completion handlers or property observers. I do not know which is the best way forward, some thoughts:

Completion handler: the pvc dismisses itself upon button-press event so the completion handler is not in the receiving (main) view controller. How do I deal with this?

Property observer: I could call the try game.execute(move) method only after the promotion piece was set (with didset) but that would make the code difficult to read and not use the nice closure the game.execute method provides.

Callbacks may be related to completion handlers, but am not sure.


Solution

  • So your block in game.execute(move: move) will fully execute which is so designed by the Sage API. You can not pause it as easy but it is doable, still let's try to solve it the other way;

    Why do you need to call the presentation of the view controller within this block? By all means try to move that away. The call try game.execute(move: move) { should only be called within processPromotion delegate method. You did not post any code but wherever this try game.execute(move: move) { code is it needs to be replaced by presenting a view controller alone.

    Then on delegate you do not even need to preserve the value newPiece = selectedType but rather just call try game.execute(move: move) { return selectedType }.

    So about pausing a block:

    It is not possible to directly "pause" a block because it is a part of execution which means the whole operation needs to pause which in the end means you need to pause your whole thread. That means you need to move the call to a separate thread and pause that one. Still this will only work if the API supports the multithreading, if the callback is called on the same tread as its execute call... So there are many tools and ways on how to lock a thread so let me just use the most primitive one which is making the thread sleep:

        var executionLocked: Bool = false
    
        func foo() {
            DispatchQueue(label: "confiramtion queue").async {
                self.executionLocked = true
                game.execute(move: move) {
                    // Assuming this is still on the "confiramtion queue" queue
                    DispatchQueue.main.async {
                        // UI code needs to be executed on main thread
                        let pvc = PromotionViewController(nibName: nil, bundle: nil)
                        pvc.delegate = self
                        pvc.modalPresentationStyle = .overCurrentContext
                        self.present(pvc, animated:true)
                    }
                    while self.executionLocked {
                        Thread.sleep(forTimeInterval: 1.0/5.0) // Check 5 times per second if unlocked
                    }
                    return self.newPiece // Or whatever the code is
                }
            }
        }
    

    Now in your delegate you need:

    func processPromotion(selectedType: Piece.Kind) {
        defer {
            boardView.isUserInteractionEnabled = true
        }
        newPiece = selectedType
        self.executionLocked = false
    }
    

    So what happens here is we start a new thread. Then lock the execution and start execution on game instance. In the block we now execute our code on main thread and then create an "endless" loop in which a thread sleeps a bit every time (the sleep is not really needed but it prevents the loop to take too much CPU power). Now all the stuff is happening on main thread which is that a new controller is presented and user may do stuff with it... Then once done a delegate will unlock the execution lock which will make the "endless" loop exit (on another thread) and return a value to your game instance.

    I do not expect you to implement this but if you will then ensure you make all precautions to correctly release the loop if needed. Like if view controller is dismissed it should unlock it, if a delegate has a "cancel" version it should exit...