State restoration and Handoff with SwiftUI
I’ve been working on a SwiftUI app for a while now, and I’ve gotten to a point where I would like to add full state restoration. I’m currently focused on the Mac app, though all of this applies to iOS as well, with some minor differences. First, to be clear, here’s an example of what I mean by state restoration:
- Someone has a window open in the app with some changes that have not been saved
- Something happens that terminates the app, for example they restart their computer for a software update
- The next time the app launches, it should restore its previous state, including their unsaved changes
State restoration is an important feature. No one should ever lose work because they didn’t save it yet.
Apple’s documentation
Apple has an article titled Restoring your app’s state with AppKit that includes a sample project. I’ve implemented this before in AppKit and UIKit. I’ve found this system straightforward and flexible.
SwiftUI has a property wrapper named SceneStorage that can accomplish something similar very easily. It works well for many things. However the window I’m trying to restore is more complex than a few text fields, and SceneStorage is not very flexible. If the view’s data is stored anywhere other than a property directly on the view, it’s not really an option. The documentation also warns against using it for model data:
Ensure that the data you use withSceneStorageis lightweight. Data of a large size, such as model data, should not be stored inSceneStorage, as poor performance may result.
I need something that’s more comparable to the state restoration features in AppKit and UIKit. These APIs make it easy to preserve the state only when it’s needed, so you don’t have to do that work every time a change is made.
Apple has an article titled Restoring your app’s state with SwiftUI that includes a sample project. The overview discusses exactly what I’m trying to implement. The sample code is iOS-only, but that’s OK. It uses SceneStorage for some things, but the article also says this:
To support state preservation and restoration, this sample usesNSUserActivityobjects. For each user activity, the app must supply an activity type defined in itsInfo.plist.
That sounds good and makes sense. NSUserActivity is already used for Handoff, which basically transfers app state from one device to another, so it makes a lot of sense to use it for state restoration on a single device.
The article goes on to describe how this works in the section “Restore the app state with an activity object.” This is where things start to get confusing. Previously we were learning “how to preserve your appʼs state information and restore the app to that previous state on subsequent launches.” The first paragraph of this section is definitely talking about the same thing. “The system saves the provided object and returns it to the app the next time it launches.” Sounds good.
The next paragraph starts talking about “handoff, Spotlight, etc.” which makes sense because NSUserActivity is used for those features as well. But as this section goes on, I find it increasingly unclear: how exactly is this used for state restoration? Perhaps we’re meant to assume it’s all the same.
The final section of the article, “Test state restoration” includes some steps you can follow to observe how it works. It does seem to work as described, though the first half of this section doesn’t explain how it works. So I’ll tell you: it uses SceneStorage. Despite everything else stated in the article, NSUserActivity is not used for state restoration.
The article ends with some steps you can follow to observe how NSUserActivity is used, but that is specifically for “Spotlight with Handoff.” One could perhaps argue that this does “restore” the app to a particular state, if you’re willing to stretch the definition of “restore.” But this really does not seem to be what the majority of the article is describing.
Other articles
It would be reasonable to assume the article is just confusingly written, and accept that SceneStorage is the only way to preserve state in SwiftUI. However “State restoration in SwiftUI” on Swift with Majid backs up Apple’s claims:
UserActivity type is another option to provide a state restoration. It allows us to mark a particular feature with unique data that the system preserves across launches.… Whenever the system relaunches the app, it passes the instance of UserActivity type with the data populated previously.
This includes sample code that’s different from Apple’s, so presumably they’ve tried it? There’s also no mention of Handoff or Spotlight in this article. So surely it works as described.
“State Restoration in SwiftUI” on Kodeko mentions “Using NSUserActivity to pass state when launching an app.” However they later explain, “You’ll use user activity, represented by the NSUserActivity class, to restore state when moving from outside your app back into it.” That’s a little bit better explained, and makes more sense with how it’s actually used.
My own testing, and a note about Handoff
Because the information about this was unclear, I spent several hours trying to answer a simple question: can NSUserActivity be used for state restoration? It didn’t seem that way, but I wanted to be sure.
After some initial tests failed, I figured I should back up and try doing something with NSUserActivity that does work: Handoff. Surprisingly, it took me several hours just to get this to work. When I couldn’t get it working in SwiftUI, I even made an AppKit app. It still wouldn’t work.
I looked at multiple articles from Apple, including Implementing Handoff in Your App, About Handoff, and Continuing User Activities with Handoff, which includes sample code. I looked at my own AppKit app. I could not figure out what I was missing. If I included a webpageURL then Handoff to Safari worked—but I couldn’t get it to hand off the activity to the app itself.
Finally I figured it out by looking at Apple’s HandoffMapViewer sample project. I spotted one capability it was using that I hadn’t enabled in my own sample app: iCloud key-value storage. I can’t find any Apple documentation that says this is required for Handoff. Occasionally it’s mentioned that iCloud is used for Handoff and that you need to be signed in to iCloud to test it. I didn’t come across anything pointing out that I needed to enable iCloud in the app. The article “Implementing Handoff in Your App” only mentions iCloud as an alternative for syncing documents. Even the sample project only mentions using it to sync favorites. As soon as I enabled iCloud key-value storage in my own sample projects, Handoff worked perfectly, both in AppKit and SwiftUI.
So now that I had Handoff working, would the same code enable state restoration? No, it does not. It can technically restore the app to a particular state, but not in the way that Apple’s article describes. SceneStorage seems to be all you get.
I will most likely end up using a separate database to store unsaved changes, but I find it frustrating that I can’t make use of features that exist in AppKit and UIKit. At minimum I need some mechanism to answer these questions:
- When is the appropriate time to save the state?
- When should saved state be discarded?
SwiftUI doesn’t provide any way to answer these questions, so it seems I’ll have to guess.
Link to this entry: https://mikepiontek.com/go/10110