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