5

I want to schedule a function call in the future. I'm using Swift.

I want to callback a method that is private and returns a Promise (from PromiseKit)

All the example I've seen use

NSTimer.scheduledTimerWithTimeInterval(ti: NSTimeInterval, target: AnyObject, selector: Selector, userInfo: AnyObject?, repeats: Bool)

Fine. I've tried

NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "connect", userInfo: nil, repeats: false)

That fails with No method declared with Objective-C selector 'connect'.

What is Objective-C doing here??

Anyway it's suggested that I add @objc in front of my method connect. Fine. Well I can't because apparently Method cannot be marked @objc because its result type cannot be represented in Objective-C

If I wanted to use objective-C I'd not be writing Swift...

There's another scheduledTimerWithTimeInterval that is

NSTimer.scheduledTimerWithTimeInterval(ti: NSTimeInterval, invocation: NSInvocation, repeats: Bool)

But from what I've read NSInvocation is not a Swift thing...

So I ended up creating a wrapper that does nothing other than calling connect and returning Void that Objective C can understand. It works but it feels very stupid. Is there a better Swift way?

Bonus: why can javascript do that as simply as setTimeout(this.connect, 1) and Swift has no built in way I can find?

Guig
  • 9,891
  • 7
  • 64
  • 126

5 Answers5

4

Beginning with iOS 10 and Swift 3, it is possible to use (NS)Timer with a block closure and thus avoid Objective-C selector invocation when the timer fires:

    if #available(iOS 10.0, *) {
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false, block: { (Timer) in
            self.connect() // per the OP's example
        })
    }

In addition to avoiding the @objc decorator, using this technique allows you to invoke methods that include non-Objective-C-compatible parameter types such as enums and optionals.

Re: setTimeout(this.connect, 1) from the Javascript, if you don't need to cancel it, a more direct analogy in Swift 3 might be:

DispatchQueue.Main.asyncAfter(deadline: .now() + 1.0, execute { self.connect() })

Which is pretty darn close given that you actually have a choice of which thread to run on ;-)

mygzi
  • 1,303
  • 1
  • 11
  • 21
1

What is Objective-C doing here??

The reason for the need of Objective-C is that it dynamically binds the "call" (it is no call in Objective-C) at runtime, while Swift cannot do this. Swift is not able to have code in the timer class that "calls" a "function" not known at the compile time of NSTimer.

BTW: NSTimer uses NSInvocation (or something similar unswifty technology) to do the the "call". Therefore the usage of NSTimer is not more swifty, but the need for late binding is more obfuscated to make Swift developers feel better.

If I wanted to use objective-C I'd not be writing Swift...

Even your code is completely written in Swift, it takes masses of benefits from Objective-C's late binding. Many of the core techniques of Cocoa are impossible to write in Swift, including responder chain, undo manager, Core Data, animations … (On the other hand you are able to define a operator, what is a big progress in software engineering and describes the whole story.)

Amin Negm-Awad
  • 16,582
  • 3
  • 35
  • 50
0

Keep in mind Swift 2.2 / Xcode 7.3 has a new way to use selector: Selector("funcName") was changed to #selector(ClassName.funcName)

You should use #selector:

NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(YourClass.connect), userInfo: nil, repeats: false)

OR Selector("connect"), however keep in mind that you'll receive a warning:

NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("connect"), userInfo: nil, repeats: false)

Also look at this in order to know how to use Selector().

More info Referencing the Objective-C selector of a method here.

Community
  • 1
  • 1
gotnull
  • 26,454
  • 22
  • 137
  • 203
  • Yes both need the selector to be understandable by Objective-C, which was not my case since it was returning a Promise (from PromiseKit) – Guig Jul 05 '16 at 06:17
  • :) Sure. I mentioned it above though: "Well I can't because apparently `Method cannot be marked @objc because its result type cannot be represented in Objective-C`" but I agree code is easier to read – Guig Jul 05 '16 at 17:17
0

There are 2 ways you can invoke a timer:

// Specify the selector name as String
// Swift will give you a warning but this is handy if you have the function's name stored
// as a variable or need some dynamism
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("timerFired"), userInfo: nil, repeats: false)

// The recommended way since Swift 2.2 / 2.3 (can't remeber exactly)
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(MyClass.timerFired), userInfo: nil, repeats: false)

And both assumes you have a function like this:

func timerFired() {
    print("Hello world")
}
Code Different
  • 90,614
  • 16
  • 144
  • 163
0

Callback for NSTimer needs to be accessible to Objective-C, even if you're using swift. In my case it meant two things:

  • decorate the private method with @objc since by default private methods are not accessible to Objective-C from Swift classes.
  • wrap my method in a method that calls it and returns nothing, so that the callback returns void. This was necessary since Objective-C didn't know how to handle the Promise type.

So at the end it looked like:

import PromiseKit

class bla : NSObject {
  private func myCallback() -> Promise<Void> {
    return Promise { fullfill, reject in
      // ...
    }
  }

  @objc private func myCallbackWrap -> Void {
    myCallback()
  }

  func startTimeout() -> Void {
      NSTimer.scheduledTimerWithTimeInterval(10, target: self, selector: #selector(myCallbackWrap), userInfo: nil, repeats: false)
  }
}
Guig
  • 9,891
  • 7
  • 64
  • 126