How can I run code after the return statement in a function in Swift?

Viewed 6583

Consider the following code:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let validator:NSPredicate = NSPredicate(format:"SELF MATCHES %@","[A-Za-z0-9- ]+")
    if(validator.evaluateWithObject(string) || string == "" /* i.e. backspace */) {
        self.process(textField)
        return true
    }
    else {
        return false
    }
}

I want to actually run self.process(textField) AFTER the return statement, because before it, the text in the textField has not actually changed yet. This led me to wonder, why can't I just execute some code after the return statement? Why do functions always stop when the return statement happens?

I realize that's traditionally what return means, but is there an alternative? Like, is there a way to return a value from a function and then still keep going?

On the one hand this seems like a stupid question, but on the other hand, I feel like I can't be the first person to ever want to do this. It would be good enough if I could fire off something to run on the next cycle of the run loop, so maybe there's something in GCD that would help me.

6 Answers

The answer to your question is no. But the solution to your problem is simple.

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let validator:NSPredicate = NSPredicate(format:"SELF MATCHES %@","[A-Za-z0-9- ]+")
    if(validator.evaluateWithObject(string) || string == "" /* i.e. backspace */) {
        DispatchQueue.main.async {
            print("about to process textField, timestamp: \(Date().timeIntervalSince1970)")
            self.process(textField)
        }
        print("about to return true, timestamp: \(Date().timeIntervalSince1970)")
        return true
    }
    else {
        return false
    }
}

DispatchQueue.main.async will defer execution to the next iteration of the run loop. With those print() statements you'll see this in the console. Your timestamps will be different but you'll see the small difference (about 15/1000 of a second in this case).

about to return true, timestamp: 1612578965.103658

about to process textField, timestamp: 1612578965.1188931

If you require a specific delay use DispatchQueue.main.asyncAfter

The best explanation I have found on this is Matt Neuburg's book "iOS 14 Programming Fundamentals with Swift". See the Delayed Performance section.

Defer injections should be at the reachable statements of the code, otherwise they will not be executed at the end of the block. Basically, its the main idea of defer.

Once a function returns, that is the end of that function's called life. To guarantee the execution of something first after a function's return would require the object that called the function to execute that something in a serialized environment.

However, in your use case, that function is invoked by user input and unless we subclass or swizzle our way through this, what I'd recommend in your use case is async dispatching with a non-noticeable delay:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let validator:NSPredicate = NSPredicate(format:"SELF MATCHES %@","[A-Za-z0-9- ]+")
    if(validator.evaluateWithObject(string) || string == "" /* i.e. backspace */) {
        self.process(textField)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            // async allows control to fall through
            // delay ensures call after return
        }
        return true
    }
    else {
        return false
    }
}

This is a borderline hack and I personally don't like this kind of solution but the alternative is so much uglier and from the user's end, it would appear seamless. There is definitely room in engineering for heuristics and this is one such case.

Related