How to store a Swift struct in UserDefaults
Learn about the pitfalls when using Codable types in UserDefaults.
09 Jan 2023 · 4 min read
The UserDefaults class provides an easy-to-use API to persistently store key-value pairs when developing iOS applications.
When we try to store a Swift struct in UserDefaults, the compiler allows us to do that:
struct Content {let title: String}let content = Content(title: "Some title")let userDefaults = UserDefaults.standarduserDefaults.set(content, forKey: "content")
But when we build and run the app, the app crashes with the following exception:
Exception: Attempt to insert non-property list object for key content
Let's look at how to solve that.

Out of the box, UserDefaults only supports data types which are also supported by property lists. Those are: Bool, Float, Double, Int, String, URL, Date and Data.
We can also store arrays and dictionaries but only if their elements are one of the types listed above.
To store a Swift struct in UserDefaults, we need to convert it to one of the supported types.
We can do that by using the Codable protocol and a JSON encoder to convert it to a Data type:
struct Content {let title: String}if let contentData = try? JSONEncoder().encode(content) {userDefaults.set(contentData, forKey: "content")}
To read the stored data, we can use a JSON decoder:
if let contentData = userDefaults.object(forKey: "content") as? Data,let content = try? JSONDecoder().decode(Content.self, from: contentData) {print(content)}
The code above works great, but there is a pitfall with this approach.
If we change the struct in the future, for example by adding another property to Content, the JSON decoder will not be able to decode a previously saved object that only had a title:
struct Content: Codable {let title: Stringlet description: String}
To solve this, we could define description as optional.
Depending on the context, declaring all future adjustments as optional might not be what we want to represent the Content type.
That's why it might be a better approach to stick to the supported UserDefaults types after all, for example by building a wrapper function that handles writing and reading the Content type. That might look as follows:
func writeContent(_ content: Content) {userDefaults.set(content.title, forKey: "contentTitle")userDefaults.set(content.description, forKey: "contentDescription")}func readContent() -> Content {let title = userDefaults.string(forKey: "contentTitle") ?? ""let description = userDefaults.string(forKey: "contentDescription") ?? ""return Content(title: title, description: description)}
In the approach shown above, we can provide default values for properties that are missing.
By going back to using supported types, we gain more safety and flexibility for future developments.

Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips