Many mobile applications base their work on quick user actions: Open the app, do something quickly, close it, and forget about it for a while. Others apps, though, have a more ambitious goal: to become a handy companion to the user. The user stores valuable information in that app, and comes back to it on a regular basis.
When dealing with the applications in the second category, you would usually expect a better-than-average user experience. Specifically, you want the app’s state to be preserved across its launches. But unfortunately, that’s not a default behavior in iOS. Apple’s mobile operating system pursues principles of effective utilization of device resources, and when an application is closed by the user, the system might terminate it any moment in order to free up memory for another application that is currently in use. When it happens, all data that wasn’t explicitly saved on disk is gone.
Let’s look at how we as developers can walk an extra mile in order to provide our users with a better experience by retaining app data.
Traditional Methods
The UI of an app can be determined by flags or stored values that indicate one state or another. In the case of basic types—for instance, user preferences—you can use UserDefaults
. This is a lightweight, key-value database provided by iOS to every application on a given device. It’s simple to use:
UserDefaults.standard.set("Yellow", forKey: "Background")
For a larger quantity of data, you can save the data directly in a file in one of the standard folders at the app’s disposal: the documents directory, the temporary directory, or similar. The syntax is more complicated in this case, because you need to manually decode values into data, and write the latter to a file on disk. For example:
let folderURL = FileManager
.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = folderURL.appendingPathComponent("Data")
let string = "I want to store this string"
try string.write(to: fileURL,
atomically: true,
encoding: .utf8)
State Preservation in UIKit
For UIKit’s view controllers, Apple gave us an app state preservation and restoration API, which includes several steps:
-
The functionality must be enabled in the
AppDelegate
:func application(
_ application: UIApplication,
shouldSaveSecureApplicationState coder: NSCoder
) -> Bool {
return true
}
func application(
_ application: UIApplication,
shouldRestoreSecureApplicationState coder: NSCoder
) -> Bool {
return true
} -
Every restorable view controller must be assigned a restoration ID in Storyboard or in code.
-
Encoding and decoding of necessary data must be implemented in restorable view controllers:
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
// Save data.
}
override func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
// Restore data.
}
This is, of course, a simplified overview. In real-world applications, using this API requires significantly more code, which is in line with the overall imperative style of UIKit programming.
The SwiftUI Method
Let’s see whether the modern SwiftUI interface offers something friendlier.
AppStorage
Everything said about UserDefaults
and file storage above is, of course, true for SwiftUI apps, too. To simplify the usage of the former, SwiftUI has a dedicated property wrapper: @AppStorage
. This wrapper can be added to any property, and it will automatically reflect a value from UserDefaults
. In the example below, background
always returns the value stored in UserDefaults
by the “Background” key, or the default value—“Yellow,” if there’s no key with such name:
@AppStorage("Background")
var background = "Yellow"
As you may have guessed, assigning a new value to the property also stores it as a new value for the “Background” key. As the icing on the cake, changing the backing stored value invalidates the view, so @AppStorage
properties—similar to in the case of @State
—are observable.
SceneStorage
As mentioned earlier, UI state preservation is a good practice in achieving a better user experience, however, it’s not critical to the app’s functionality. SwiftUI’s solution, the @SceneStorage
property wrapper, is effective but lightweight, and thus is not supposed to be 100% reliable. We’ll come back to the limitations later, but first, let’s look at how it can be used.
@SceneStorage
works similarly to @AppStorage
with one important difference: The values it stores are only available to the current scene, and thus, the former is more suitable for storing data related directly to the UI of the application. Therefore, this is exactly what we need to implement state preservation in SwiftUI.
Its syntax and usage are straightforward: Just add the wrapper to any view property that needs preservation:
@SceneStorage("Background")
var background = "Yellow"
In the example above, after view initialization, the value of the background
property will be equal to its last value before the application was closed last time. This means that the system preserves the last value across app launches.
Limitations of SceneStorage
Storage keys ("Background," in the example above,) however, require attention. To avoid unexpected bugs, they must be unique across the app scene. Additionally, if you reuse your views, keys should be also unique to each view instance, otherwise the instances will share the same stored value.
This solution’s power is not limitless. It only supports a short list of types, namely Bool
, Int
, Double
, String
, URL
, and Data
. If you need to save any custom type, you should use some other storage technique.
You can, of course, encode anything to Data
and still use @SceneStorage
, but Apple explicitly warns us against doing that, and against storing anything large in general. The reason is that storing and restoring happens inside a view, and large values may result in a poor performance of the view.
From that limitation, another caveat follows: If you need to preserve something more complicated than a number or string—for example, a navigation path to open a specific screen—you must implement your own logic on how to encode and decode such values. This bears an unfortunate similarity to the “old” ways.
And finally, as the property wrapper’s name suggests, stored values are only accessible within a single scene. There are a couple of implications to this:
- If your application supports multiple scenes, each of them has its own dedicated storage which is not shared with other scenes. To share or pass data between scenes, something else would need to be implemented, for example,
NSUserActivity
. - If the corresponding scene is destroyed (when, for example, the user removed it from the switcher snapshot on iPad, where the app may have multiple scenes,) the data is gone too. Thus, no sensitive or unrecoverable data should rely on the
@SceneStorage
property wrapper.
Evidently, this @SceneStorage
method has a number of restrictions, but it still may come in handy in simple situations.
Conclusion
Old methods of preserving data to restore the state of an iOS application, although not particularly complicated, all rely on you to implement their particular logic and handle corner cases. Modern, SwiftUI-based APIs aim to achieve similar results, and they indeed appear simpler and more concise. However, they have similar limitations to the old methods, and require extra work in more complicated cases. I personally think that the current methods of state preservation in SwiftUI are just a start, and something even better awaits us in a future release of the iOS SDK.
Logging
In some cases you might want to log changes of preserved values to later analyze the state of the UI or to gather additional information necessary to nail a nasty bug. Unfortunately, neither the old method of UI state preservation nor the new one offer a means of doing that automatically. You need to track the change manually, e.g., within the onChange(of:perform:)
SwiftUI view modifier, and pass it further to your log system. For example, Shipbook offers a simple in use yet powerful platform to gather your app logs remotely.