How to properly fire/call a "selector" in Swift? - ios

Question Summary:
If you have a Swift class that takes a selector as an argument in its initializer, how do you manually "fire/call" that selector?
Full Question:
Consider the following attempt at making a custom timer in Swift:
let TIME_INTERVAL = 0.1
class ValueAnimator : NSObject {
private var timer = Timer()
private let maxRep: Int
private var currentRepIndex: Int = 0
private var selector: Selector
init(durationInSeconds: Int, selector: Selector) {
print("VALUEANIMATOR INIT")
self.maxRep = Int(Double(durationInSeconds) / TIME_INTERVAL)
self.selector = selector
}
func start() {
timer = Timer.scheduledTimer(timeInterval: TIME_INTERVAL, target: self, selector: (#selector(timerCallback)), userInfo: nil, repeats: true)
}
#objc func timerCallback() {
currentRepIndex += 1
perform(selector) // <-------- this line causes crash, "unrecognized selector sent to instance 0x600001740030"
print ("VA timer called!, rep: \(currentRepIndex)")
if currentRepIndex == maxRep {
timer.invalidate()
print("VA timer invalidated")
}
}
}
The usage of this "ValueAnimator" would be similar to a normal Timer/NSTimer, in that you pass a "selector" as an argument and that selector is called each time the ValueAnimator fires:
[In Parent Class]:
// { ...
let valueAnimatorTest = ValueAnimator(durationInSeconds: 10, selector: #selector(self.temp))
valueAnimatorTest.start()
}
#objc func temp() {
print("temp VA callback works!") // this doesn't happen :(
}
I'm trying to implement the same thing and as I understand, the line:
perform(selector)
should fire the selector in the parent class, but instead I get the error: "unrecognized selector sent to instance 0x600001740030"
I'm in a bit over my head here. I have tried googling the error, but everyone seems to be talking about how to use a selector from the parent-side (how to use Timer.scheduledTimer(), etc.) but I already know how to do that successfully.
I've also tried various tweaks to the code (changing public/private, scope of variables, and different forms of the performSelector() function)... but can't figure out the proper way to make the selector fire... or the unrelated mistake I've made if there is one.
Thanks for any help.

By calling perform(selector) it's like you're calling self.perform(selector) (self is implied), and by doing so the current instance of the ValueAnimator class is the object that actually performs the selector. When that happens, it tries to call a method called temp() of the ValueAnimator class, but as it doesn't exist the app is crashing.
You can verify that if you add a temp() method in the ValueAnimator:
#objc func temp() {
print("Wrong method!!!")
}
If you run now you'll have no crash and the "Wrong Selector!!!" message will appear on the console.
The solution to your problem is to pass the object that should run the selector method along with the selector to the initialisation of the ValueAnimator object.
In the ValueAnimator class declare the following property:
private var target: AnyObject
Update the init method so it can get the target as an argument:
init(durationInSeconds: Int, selector: Selector, target: AnyObject) {
...
self.target = target
}
Also update the timerCallback():
#objc func timerCallback() {
...
_ = target.perform(selector)
...
}
Finally, when you initialise a ValueAnimator instance pass the object that the selector belongs to as well:
let valueAnimatorTest = ValueAnimator(durationInSeconds: 10, selector: #selector(self.temp), target: self)
Run again and the proper temp() method will be executed this time.
I hope it helps.

You are calling perform on the wrong object: its an instance method of NSObject, so you are trying to call perform on ValueAnimator and ValueAnimator does not respond to "temp". You must pass in both the object and the selector you want to perform, then you call perform on that object with the selector. Notice that this is exactly what Timer does: you have to pass in self as the object and the timer call the selector you specify on self.

Related

Function with Completion in Timer Selector

I am declaring a function as such:
#objc func fetchDatabase(completion: ((Bool) -> Void)? = nil)
I'm allowing the completion to be nil so I can either call it as fetchDatabase() or as
fetchDatabase(completion: { (result) in
// Stuff in here
})
However, I am also trying to use this function in a #selector for a Timer. I am creating this timer using the following line:
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(fetchDatabase), userInfo: nil, repeats: true)
Without the completion, this runs fine. However, with the completion added, I get an EXC_BAD_ACCESS error whenever the timer is run. Some help with correctly constructing this selector would be greatly appreciated, if this is in fact the error.
The selector passed to the timer only allows one of two possible signatures
someFunction()
someFunction(someLabel someParameter: Timer)
You can't pass your fetchDatabase(completion:) function because it doesn't match either of the two legal selector signatures.
You need to pass a valid selector which in turn calls your fetchDatabase(completion:) function. For example:
#objc timerHandler() {
fetchDatabase(completion: { (result) in
// Stuff in here
})
}
use #selector(timerHandler) with your timer.
Default arguments get applied at the calling site, so you'll need to generate two separate methods (one of which calls the other):
func fetchDatabase() { fetchDatabase(callback:nil) }
func fetchDatabase(callback:()->()) {
...
}
Now your scheduledTimer call should work fine.

How to test if a UIControlEvents has been fired

I have a library implementing a custom UIControl with a method which would fire a .valueChanged event when called. I would like to test the method for that behavior.
My custom control:
class MyControl: UIControl {
func fire() {
sendActions(for: .valueChanged)
}
}
And the test:
import XCTest
class ControlEventObserver: NSObject {
var expectation: XCTestExpectation!
init(expectation anExpectation: XCTestExpectation) {
expectation = anExpectation
}
func observe() {
expectation.fulfill()
}
}
class Tests: XCTestCase {
func test() {
let myExpectation = expectation(description: "event fired")
let observer = ControlEventObserver(expectation: myExpectation)
let control = MyControl()
control.addTarget(observer, action: #selector(ControlEventObserver.observe), for: .valueChanged)
control.fire()
waitForExpectations(timeout: 1) { error in
XCTAssertNil(error)
}
}
}
The problem is the observe method never gets called so the expectation is not fulfilled.
The question is: how can we test for UIControlEvents like in this case? Perhaps we need to force the runloop somehow?
EDIT 1:
Please note that since I am testing a library, my test target does not have any Host Application. The test above passes when the test target has a host application.
Apple's documentation for UIControl states that:
When a control-specific event occurs, the control calls any associated
action methods right away. Action methods are dispatched through the
current UIApplication object, which finds an appropriate object to
handle the message, following the responder chain if needed.
When sendActions(for:) is called on a UIControl, the control will call the UIApplication's sendAction(_:to:from:for:) to deliver the event to the registered target.
Since I am testing a library without any Host Application, there is no UIApplication object. Hence, the .valueChanged event is not dispatched and the observe method does not get called.
You are declaring the observer object inside the test method. This means that as soon as the method completes it will be released from memory and hence is not called. Create a reference to the observer at class level in the Tests class as follows and it will work.
class Tests: XCTestCase {
var observer: ControlEventObserver!
func test() {
let myExpectation = expectation(description: "event fired")
self.observer = ControlEventObserver(expectation: myExpectation)
let control = MyControl()
control.addTarget(observer, action:#selector(ControlEventObserver.observe), for: .valueChanged)
control.fire()
waitForExpectations(timeout: 1) { error in
XCTAssertNil(error)
}
}
}
You will also need the myExpectation & control to be declared in the same way else that won't be called either.

Does the target of #selector() depend on the context of `addObserver` (class vs object)?

The full test case below is supposed to demonstrate: a selector, even though it is specified identically in two places, is performed differently: either it is performed on the class, or on the object. (I understand that a static method and an object method can share the same name, but there is only one below.) Whether the receiver is class or object seems to depend on where the “same” selector is made known to NSNotificationCenter, either in class context or in method context:
a static method has the call to addObserver, or
an object method has the call addObserver
while the calls are otherwise identical.
If the identical call occurs in a static method, then when the notification is processed later, the system tries to invoke the selector on the class, not the object. The class does not have it. The code compiles fine with the new (in 2.2) syntax. Is this result to be expected?
import XCTest
import class Foundation.NSNotificationCenter // for emphasis
class SelectorTests: XCTestCase {
static let NotificationName = "OneTwoThreeNotification"
override func setUp() {
super.setUp()
}
override func tearDown() {
NSNotificationCenter.defaultCenter().removeObserver(self)
super.tearDown()
}
func addObserverForTestNormal() { // <- HERE
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(SelectorTests.myMethod(_:)), // <- HERE
name: SelectorTests.NotificationName,
object: nil)
}
func testNormal() {
self.addObserverForTestNormal()
NSNotificationCenter.defaultCenter().postNotificationName(
SelectorTests.NotificationName,
object: self)
}
static func addObserverForTestStatic() { // <- HERE
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(SelectorTests.myMethod(_:)), // <- HERE
name: SelectorTests.NotificationName,
object: nil)
}
func testStatic() {
SelectorTests.addObserverForTestStatic()
NSNotificationCenter.defaultCenter().postNotificationName(
SelectorTests.NotificationName,
object: self)
}
func myMethod(x : Int) {
XCTAssert(true)
}
}
One test succeeds, the other fails. The gist of the stack trace and message is
"+[KuckuckTests.SelectorTests myMethod:]: unrecognized selector sent to class
Is this schism, i.e. class or object “inferred” from addObserver-context, so obvious to old Objective-C hands that it isn't worth mentioning with #selector? In this case, could you point out some documentation?
Edit: just noticed that self in the static function's invocation
of addObserver is perhaps referring to the class, not to some object. That makes the effect somewhat plausible, and suggests that programmers should know what overloaded names stand for…
Nothing about a #selector expression has any connection to the use site of that selector. A selector names a message, and says nothing about the receiver of that message. You can use a #selector expression to create a Selector value for a method on one object, then pass that Selector value to an API (like NSNotificationCenter.addObserver or UIControl.sendAction or NSTimer.init) that'll result in sending a message with that selector to some completely different object.
This loose binding is an intentional part of the dynamic nature of the Objective-C runtime Cocoa uses for passing these messages (regardless of whether the functions referenced by your selectors are build in ObjC or Swift). The #selector expression, and the Swift function-reference syntax it depends on, give you a way to "sorta" strongly type your use of selectors, but only on one end — they let you verify that the Selector value you're constructing refers to a specific method. (But once you have a Selector value, how it gets used is out of Swift's control.)
Your error message (emphasis added):
unrecognized selector sent to class
...indicates that the failure is because the message is being sent to the SelectorTests class object (aka the metaclass object). That is, by scheduling a notification to be sent to self in a static method, you're asking for a call to class func myMethod, not to func myMethod.
The self keyword always refers to the instance responsible for the code that's executing: inside an instance method, self refers to the current instance. Inside a class method, self refers to the (only instance of) the class object.

Call class method with argument from another class method

In my code file MyItemVC.swift I have defined the following class and method:
class MyItemVC: UIViewController, UITextViewDelegate {
var timer = NSTimer()
func cycleTimer(toggleOn: Bool) {
if toggleOn == true {
// Timer calls the replaceItem method every 3 seconds
timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("replaceItem"), userInfo: nil, repeats: true)
} else {
timer.invalidate() // stop the timer
}
}
}
Elsewhere in this class, I call cycleTimer(true) to start the timer and cycleTimer(false) to stop it.
Now, I also want to use the usual methods in my AppDelegate.swift code file to start and stop the timer when the app moves from active to inactive state. But I'm having trouble calling the cycleTimer method from that class.
I found an answer on Stack Overflow that suggested I could call it like this:
func applicationWillResignActive(application: UIApplication) {
MyItemVC.cycleTimer()
}
But I also need to pass in an argument. So I tried calling it like this:
func applicationWillResignActive(application: UIApplication) {
MyItemVC.cycleTimer(true)
}
But I got this error:
Cannot invoke 'cycleTimer' with an argument list of type '(Bool)'
How can I call this method from the AppDelegate methods while passing in an argument?
Thanks for the help. I realize this must be a very basic question but I'm new to programming and trying to teach myself using Swift. An answer using Swift rather than Obj-C would be greatly appreciated.
You need to use class function to be able to use it this way.
class func cycleTimer(toggleOn: Bool) {
However, I'm not sure about thread safety.
The function you have specified is not a class function. Add class keyword before func keyword.
The changed code:
class func cycleTimer
Note: In the previous versions of Swift you must use the following code (and also in C or other languages):
static func cycleTimer

Swift - NSTimer to loop through array with delay

I have an array, and I wish to set the text of a UILabel to an element of the array, and then after a second set the text as the next element of the array. Once the end of the array has been reached, it needs to then return to the start. I have tried completing this with a for loop that runs through the array, with a delay function inside the for loop, but this does not slow down the operation of the for loop itself. I have also tried using an NSTimer,
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("update"), userInfo: nil, repeats: true)
func update() {
var i = Int()
UIView.animateWithDuration(0.2, delay: 0.3, options: nil, animations: { () -> Void in
if i == connectionName.count - 1 {
i = 0
println(connectionName[i])
} else {
println(connectionName[i])
}
}, completion: { (finished: Bool) -> Void in
i = i+1
})
}
But I just get an error
2015-01-08 15:06:10.511 Tinder[585:10642] -[Tinder.TinderViewController update]: unrecognized selector sent to instance 0x7fae99ead3f0
2015-01-08 15:06:10.612 Tinder[585:10642] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Tinder.TinderViewController update]: unrecognized selector sent to instance 0x7fae99ead3f0'
Is this because the function is defined within the view did load method?
You asked
Is this because the function is defined within the view did load method?
That is indeed the problem. NSTimer uses Objective-C messaging to call the timer function, and
nested functions in Swift are not exposed as Objective-C methods.
You have to define update() as a top-level function within the view controller class.
Unfortunately, the compiler cannot warn you, because it does not "know" that the
string "update" in the selector corresponds to the update() function. (Unlike the Objective-C #selector(), Swift uses simple strings as selectors, and the compiler
cannot verify its existence).
If you explicitly annotate a nested function with #objc then you will get a
compiler error.

Resources