Logo for tanaschita.com

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.standard
userDefaults.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.

Sponsorship logo
Preparing for a technical iOS job interview
Check out my new book on preparing for a technical iOS job interview with over 200 questions & answers. Test your knowledge on iOS topics such as Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, Authentication, Core Data, Concurrency with async/await, Security, Automated Testing and more.
LEARN MORE

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: String
let 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.

Sponsorship logo
Preparing for a technical iOS job interview
Check out my new book on preparing for a technical iOS job interview with over 200 questions & answers. Test your knowledge on iOS topics such as Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, Authentication, Core Data, Concurrency with async/await, Security, Automated Testing and more.
LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

core data

persistence

swift

ios

Get started with NSPredicate to filter NSFetchRequest in Core Data

Get a quick overview on how to use predicates to filter fetch request results.

20 Mar 2023 · 3 min read

Latest articles and tips

© 2023 tanaschita.com

Privacy policy

Impressum