Swift 3: Convenience Initializer Extending Foundation's 'Timer' Hangs

626 views Asked by At

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.

3

There are 3 answers

1
matt On BEST ANSWER

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...

3
Jerry Krinock On

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?

0
Anurag On

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