3

I am attempting to extend Foundation's Timer class in Swift 3, adding a convenience initializer. But its call to the Foundation-provided initializer never returns.

The problem is illustrated in the following trivialized demo, which can be run as a Playground.

import Foundation

extension Timer {
    convenience init(target: Any) {
        print("Next statement never returns")
        self.init(timeInterval: 1.0,
                  target: target,
                  selector: #selector(Target.fire),
                  userInfo: nil,
                  repeats: true)
        print("This never executes")
    }
}

class Target {
    @objc func fire(_ timer: Timer) {
    }
}

let target = Target()
let timer = Timer(target: target)

Console output:

Next statement never returns

To study further,

• I wrote similar code extending URLProtocol (one of the only other Foundation classes with an instance initializer). Result: No problem.

• To eliminate the Objective-C stuff as a possible cause, I changed the wrapped initializer to init(timeInterval:repeats:block:) method and provided a Swift closure. Result: Same problem.

Jerry Krinock
  • 4,860
  • 33
  • 39

3 Answers3

4

I don't actually know the answer, but I see from running this in an actual app with a debugger that there's an infinite recursion (hence the hang). I suspect that this is because you're not in fact calling a designated initializer of Timer. This fact is not obvious, but if you try to subclass Timer and call super.init(timeInterval...) the compiler complains, and also there is an odd "not inherited" marking on super.init(timeInterval...) in the header.

I was able to work around the issue by calling self.init(fireAt:...) instead:

extension Timer {
    convenience init(target: Any) {
        print("Next statement never returns") // but it does
        self.init(fireAt: Date(), interval: 1, target: target, 
            selector: #selector(Target.fire), userInfo: nil, repeats: true)
        print("This never executes") // but it does
    }
}

Make of that what you will...

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • The "not inherited" marking in the header is to state that it cannot be used as an initialiser for a subclass, due to the fact that it's imported from an Obj-C factory method (`timerWithTimeInterval:target:selector:userInfo:repeats:`) which only returns `NSTimer` instances (thus cannot be used to create a subclass instance). Although I don't see why this should make it infinitely recurse (cannot reproduce with my own Obj-C factory method), therefore it's probably something in the `NSTimer` implementation doing it. Most peculiar. – Hamish Dec 25 '16 at 14:02
2

I see the same issue as described by matt with infinite recursion. -[NSCFTimer release] is called over and over on the same object. This behavior can be reproduced in pure Objective-C by calling a class initializer from within an instance initializer.

@implementation NSTimer (Foo)

- (instancetype)initWithTarget:(id)t {
    return [NSTimer timerWithTimeInterval:1 target:t selector:@selector(description) userInfo:nil repeats:NO];
}

@end

The compiler complains that a designated initializer isn't being called which very well seems related to fixing the issue but doesn't explain the recursive calls.

warning: convenience initializer missing a 'self' call to another initializer
Anurag
  • 140,337
  • 36
  • 221
  • 257
0

The answer by @matt works.

Yes, I saw that infinite recursion in my app, too – CFReleases. The Swift book is clear that convenience initializers must call designated initializers. It doesn't say what the penalty is, though. An infinite recursion, though surprising, is plausible.

However, look at these two declarations which you can see by option-clicking or command-clicking on one of these methods in Xcode:

init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)

convenience init(fire date: Date, interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)

I think something is wrong there. The function @matt suggested, with the additional ("fire") parameter, which solves the problem for me, is marked convenience. The function I used, which is not marked convenience, I presume (as a Swift newbie) is therefore designated. But it has one less parameter. Huh?

I think I shall file a bug stating that Apple somehow got the convenience keyword on the wrong function. This may be possible because Swift really doesn't have header files, correct? So I wonder what we're seeing when we option-click on a Foundation function. Possibly there is a step in their workflow which is susceptible to human error?

Jerry Krinock
  • 4,860
  • 33
  • 39
  • The Swift book says **Rule2: A convenience initializer must call another initializer from the same class.** and **Rule3: A convenience initializer must ultimately call a designated initializer.** The rules does not inhibit a convenience initializer calling another convenience initializer of the same class. And in other classes, defining a convenience initializer which calls another convenience initializer actually works. So, you should send a Bug Report with the original issue. `Timer` is a special case. How generated headers are shown is a subtle thing and would be ignore. – OOPer Dec 25 '16 at 08:42
  • You are correct, @OOPer. All initializers should *eventually* call a designated initializer, so as long as my convenience initializer calls another initializer, designated or not, it should work. – Jerry Krinock Dec 25 '16 at 15:08
  • 2
    I've now filed **Apple Bug Reporter Problem ID 29804952** on the original issue. In *Additional Notes* I asked how function X has one less parameter than function Y, but function Y is the one marked *convenience*. Thank you all. – Jerry Krinock Dec 25 '16 at 15:16