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.
Related
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.
Does exist any solution to make reusable protocol extension for more classes with selectors which would point to itself?
For example I am trying to make extension TimerHelper which adds appropriate functions to work with NSTimer. I found this:
https://forums.developer.apple.com/thread/26983
https://forums.developer.apple.com/message/49465#49465
But solution seems a bit twisty...
What I am trying to make in code, which doesn't work of course, is something like this:
protocol TimerHelper {
var timer:NSTimer { get set }
}
extension TimerHelper {
func startTimer() {
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: self.updateTimer(), userInfo: nil, repeats: true)
}
func updateTimer() {
print("Timer updated.")
}
}
class ViewController: UIViewController, TimerHelper {
var timer: NSTimer = NSTimer()
func start() {
startTimer()
}
}
Thanks
Never do:
... = NSTimer()
Instead create the variable as an optional. You want to invalidate and destroy the timer, and you never want a timer that hasn't been configured properly or invalidated.
Your extension is inappropriate because it deals with aspects not covered by the protocol itself. You should have 2 protocols, where the second protocol extends the first and is called something like TimerActivation. It defines the functions and the extension implements them.
This doesn't change what you need to do in the VC to use the timer, but it makes your type system clean, effective, reusable and extensible.
For the self referential part you need to look at #selector, I haven't tried it inside a protocol before, should be interesting...
It may be wiser to supply the selector, or an invocation, to the start function, because there is little point in a protocol extension implementing the selector when the protocol is so general. But, I suppose you may want to add other child protocols with extensions which provide other implementations so you can mixin functionality, interesting idea...
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
I need to add an NSTimer in the applicationDidEnterBackground method; I tried adding
NSTimer.scheduledTimerWithTimeInterval(5, target: self, selector: Selector("test"), userInfo: nil, repeats: true)
But it's not working.
According to your comments, your declaration should look something like this:
NSTimer.scheduledTimerWithTimeInterval(60, target: self, selector: "test:", userInfo: nil, repeats: false)
Note two things:
You can pass a string literal to the selector parameter directly (no need for Selector() syntax), but this is of course optional.
The NSTimer expects the selector that will fire when it hits zero to accept a parameter (of type NSTimer) to which it will pass itself, so a colon (:) is needed at the end of the selector name to indicate that it will take a parameter.
The selector should have the following signature: timerFireMethod:
(including a colon to indicate that the method takes an argument). The
timer passes itself as the argument, thus the method would adopt the
following pattern: - (void)timerFireMethod:(NSTimer *)timer
From the NSTimer documentation.
Because of this, you'll also have to modify your function declaration a bit:
func test(timer: NSTimer) {
// Proceed to grab location from background
}
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.