Joe
Joe

Reputation: 1968

cancelPreviousPerformRequests does not seem to work in Swift 3.0

As the title states, for some reason, the following (simplified) code is not working:

extension InputView: {

    func updateTable(text: String) {
            NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(loadPlaces(text:)), object: nil)
          //NSObject.cancelPreviousPerformRequests(withTarget: self)

            self.perform(#selector(loadPlaces(text:)), with: text, afterDelay: 0.5)

            prevSearch = inputField.text!;
        }

    //Private wrapper function
    @objc private func loadPlaces(text: String) {
        print("loading results for: \(text)")
       // locator?.searchTextHasChanged(text: text)
    }
}

I call updateTable every time a user edits a UITextField, which calls localPlaces which calls a function that queries google's online places API (commented out). Unfortunately, the print line in loadPlaces is called after every single call to updateTable. From my visual inspection, it seems there is in fact a delay to the print statements, however, the old calls do not cancel. I've tried looking on a lot of StackOverflow threads but I couldn't find anything updated for Swift 3. Am I calling something incorrectly?

PS. If I instead use the commented out, single-argument, cancelPreviousPerformRequests. It works for some reason.


Edit: I have been able to replicate this error in a separate project. So I'm 100% sure that the above code is wrong. If you would like to replicate this error, open up a new iOS project and paste the following code into the default ViewController:

class InputView: UIView {

    func updateTable(text: String) {
        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(loadPlaces(text:)), object: nil)

        self.perform(#selector(loadPlaces(text:)), with: text, afterDelay: 0.5)

    }

    //Private wrapper function
    @objc private func loadPlaces(text: String) {
        print("loading results for: \(text)")
        // locator?.searchTextHasChanged(text: text)
    }
}




class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let input = InputView()

        for i in 0..<200 {
            input.updateTable(text: "Call \(i)")
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

Upvotes: 4

Views: 4314

Answers (2)

Duncan C
Duncan C

Reputation: 131471

EDIT:

The first part of this answer is wrong. See the edit at the bottom for updated information. I'm leaving the original answer since the discussion might be useful.

It looks to me like there is a bug in the way NSObject maps Swift function names to selectors that is preventing this from working correctly. The only way I was able to get the cancelPreviousPerformRequests function to actually cancel the pending perform() is if the function does not have any parameters. If the function takes a single anonymous parameter or a named parameter then the cancelPreviousPerformRequests function does not cancel the pending perform(_:with:afterDelay:).

Another bug I've found: If you use a function with an anonymous parameter, e.g.:

func foo(_ value: String) {
  print("In function \(#function)")
}

Then the result you see in the print statement is:

In function foo

You'll see the same thing if the function has 2, 3, or more anonymous parameters.

If you have a function with no parameters, you get a different result:

func foo() {
  print("In function \(#function)")
}

That code will display the message:

In function foo()

(Note the parentheses after the function name.)

EDIT

Note that it seems I was wrong. Apparently the object parameter to cancelPreviousPerformRequests must match what was passed in. You can only pass object:nil to cancelPreviousPerformRequests if the selector was invoked with a nil argument.

To quote the docs:

The argument for requests previously registered with the perform(:with:afterDelay:) instance method. Argument equality is determined using isEqual(:), so the value need not be the same object that was passed originally. Pass nil to match a request for nil that was originally passed as the argument.

Upvotes: 1

OOPer
OOPer

Reputation: 47896

The explanation in Duncan C's answer is not appropriate for this case.

In the reference of cancelPreviousPerformRequests(withTarget:selector:object:):

Discussion

All perform requests are canceled that have the same target as aTarget, argument as anArgument, and selector as aSelector.

So, when you have a line like:

<aTarget>.perform(<aSelector>, with: <anArgument>, afterDelay: someDelay)

You can cancel it with:

NSObject.cancelPreviousPerformRequests(withTarget: <aTarget>, selector: <aSelector>, object: <anArgument>)

only when all 3 things aTarget, aSelector and anArgument match.


Please try something like this and check what you see:

class InputView: UIView {
    
    var lastPerformArgument: NSString? = nil
    
    func updateTable(text: String) {
        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(loadPlaces(text:)), object: lastPerformArgument)

        lastPerformArgument = text as NSString
        self.perform(#selector(loadPlaces(text:)), with: lastPerformArgument, afterDelay: 0.5)
        
    }
    
    //Private wrapper function
    @objc private func loadPlaces(text: String) {
        print("loading results for: \(text)")
        // locator?.searchTextHasChanged(text: text)
    }
}

Upvotes: 13

Related Questions