Reputation: 1968
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
Reputation: 131471
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.)
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
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 asanArgument
, and selector asaSelector
.
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