Catch 22 - creating a Timer in class initialisation - cannot refer to self in closure

Hello,

A class must set all its properties to sensible values in its init() method, and you cannot refer to self in the init() method before all the properties are assigned sensible values.

So, how does one go about having a Timer as a property of a class that calls an instance method of that class when it fires?

e.g.

import Foundation

class RepeatingThing {
	var aTimer: Timer
	let timerDuration = 2.0

	init() {
		aTimer = Timer.scheduledTimer(withTimeInterval: timerDuration, repeats: true, block: {_ in self.timerDoSomething() } )
	}

	func timerDoSomething() {
		print("The timer fired!")
	}
}


let repeater = RepeatingThing()

Doesn't compile due to:
'self' captured by a closure before all members were initialized

I can't initialise the member without using a closure, but I can't use a closure because I haven't initialised the member.

Noting that you are declaring your aTimer property as a variable, I would point out that you can solve this example by changing that line thusly:
var aTimer: Timer? = nil

Personally, I have elected to use DispatchSourceTimer instead in a similar situation, as its scheduling is separate from its initialization.

OK, I think I've worked it out - it's not elegant, but it seems to work:

I have extracted the timer initialisation from out of init() and made it lazy.

	lazy private var aTimer: Timer = {
		var timer:Timer
		if #available(OSX 10.12, *) {
			print("Using closure timer.")
			timer = Timer.scheduledTimer(withTimeInterval: timerDuration, repeats: true, block: {_ in self.timerDoSomething() } )
		} else {
			print("Using selector timer.")
			timer = Timer(timeInterval: timerDuration, target: self, selector: #selector(timerDoSomething), userInfo: nil, repeats: true)
		}
		return timer
	}()
1 Like

Thanks - I considered that but shied away as the idea of having to unwrap an optional every time I wanted to refer to the timer (which I could guarantee would be non-nil.) I suppose one could make a 'shadow' optional Timer and then have a computed timer property which force unwraps the shadow timer....

Another "possibility" is to assign a bogus Timer:
var aTimer = Timer(fire: .distantFuture, interval: 0, repeats: false, block: { _ in })
(and then invalidate before re-assigning, just to be safe.)

As for the DispatchSourceTimer version, you'd get

timer = DispatchSource.makeTimerSource(queue: .global(qos: .default))
timer.schedule(deadline: .now() + .seconds(1), repeating: .seconds(1))
timer.setEventHandler(handler: self.timerDoSomething)
timer.resume()

Every option seems dissatisfying in some way!

1 Like

This is precisely one of the use cases served by using an implicitly unwrapped optional: var aTimer: Timer!

Personally, I have elected to use DispatchSourceTimer instead in a
similar situation, as its scheduling is separate from its
initialization.

Separating initialisation and scheduling isn’t the issue here, because Timer supports that just fine:

let t = Timer(fire: Date(), interval: 1.0, repeats: true, block: {
    … your code here …
})
RunLoop.current.add(t, forMode: .default)

The difference is that DispatchSourceTimer lets your set the event handler separate from creating the source, whereas Timer does not.

Personally I’m not a fan of the design approach outlined in the first post here: If I create a class that’s active (runs a timer, hits the network, whatever), I never want it to start on initialisation. It is much better, in my experience, to separate initialisation from scheduling. And once you do that, you have to track whether the timer is running or not, at which point having an optional Timer is the right answer.

One reason I want the separation between initialisation and scheduling is that you need to separate descheduling from deinitialisation. When dealing with active classes, relying on the deinit to shut things down is a really bad idea.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

3 Likes

Thanks, that's very helpful.

Terms of Service

Privacy Policy

Cookie Policy