Recursive Property Observation in Swift

Over the last couple of years, I’ve been writing almost exclusively Swift. It’s been quite a roller coaster. I still don’t feel as though I’m an expert in the language, but I am getting to the point where I quickly identify its quirks, and can make a reasonable effort to debug them.

This one came up fairly late in the process of building Jot. Although it’s a simple app, feature- and screen-wise, there’s still some complexity there. Eventually there were enough potential things that could happen when the app opened—authenticating with FaceID or TouchID (and the associated failure states); starting a new note, either automatically or from a deep press on the app icon; opening a note from Spotlight; displaying a reminder—that the best choice was to build a state machine specifically for foregrounding. This was a great move! I kept all my logic in one place, and it was really easy to figure out how I moved through the various states from launch to default screen.

I ran into trouble when I needed to move through states without user intervention between. I wrote it something like this:

enum StartState {
	case unauthenticated
	case authenticating
	case authenticationFailed(error: Error)
	case authenticationSucceeded
}
var startState: StartState {
	didSet {
		switch startState {
			case .unauthenticated:
				if UserDefaults.standard.requiresAuthentication {
					startState = .authenticating
				} else {
					startState = .authenticationSucceeded
				}
			case .authenticating:
				authenticate() // invoke Apple's auth, and eventually set a different state
			case .authenticationSucceeded:
				authenticated = true // we're done
		}
	}
}

Well, it turns out that after entering the unauthenticated state, the flow stopped. This is because Swift’s property observations are not recursive: once you’re in the property observer, further changes to the property won’t trigger the observer again. It’s easy to imagine why: in a simpler property, like

var doubleWhatImGiven: Int {
	didSet {
		doubleWhatImGiven *= 2
	}
}

This will cause a stack overflow. Nobody likes that. But I’m convinced I know what I’m doing, and fortunately Swift does give me enough rope to hang myself. All that’s necessary is to use an intermediate function, like so:

var startState: StartState {
	didSet {
		respondToStartState()
	}
}

private func respondToStartState() {
	switch startState {
		case .unauthenticated:
			if UserDefaults.standard.requiresAuthentication {
				startState = .authenticating
			} else {
				startState = .authenticationSucceeded
			}
...
}

And everything works great! One level of indirection is enough to convince the runtime that it ought to invoke the property observer again, even in the same run loop.

Joel Kraut

About Joel Kraut

Developing on Apple platforms for, holy shit, over ten years now. Find me on linkedin and twitter. My personal website is foon.us.
This entry was posted in Code and tagged , , . Bookmark the permalink.