Swift
UserDefaults
Struct
iOS Development
Data Persistence

Save Struct to UserDefaults

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

UserDefaults is meant for small preference-style values, not arbitrary Swift types. If you want to store a struct, the normal solution is to make it Codable, encode it into Data, save that data, and decode it when reading it back.

Why a Struct Cannot Be Stored Directly

UserDefaults only accepts property-list-compatible types directly, such as String, Int, Bool, Date, Data, arrays, and dictionaries made from those types. A custom Swift struct is not automatically one of those storage formats.

That means code like this is invalid:

swift
1struct User {
2    let name: String
3    let age: Int
4}
5
6let user = User(name: "Ana", age: 29)
7UserDefaults.standard.set(user, forKey: "currentUser")

The fix is not to force the struct into UserDefaults directly. The fix is to encode it into Data first.

Use Codable and Store Encoded Data

The most common approach uses Codable with JSONEncoder and JSONDecoder.

swift
1import Foundation
2
3struct User: Codable {
4    let name: String
5    let age: Int
6}
7
8let defaults = UserDefaults.standard
9let encoder = JSONEncoder()
10
11let user = User(name: "Ana", age: 29)
12let data = try encoder.encode(user)
13defaults.set(data, forKey: "currentUser")

Reading the value back is the reverse operation:

swift
1import Foundation
2
3let defaults = UserDefaults.standard
4let decoder = JSONDecoder()
5
6if let data = defaults.data(forKey: "currentUser") {
7    let savedUser = try decoder.decode(User.self, from: data)
8    print(savedUser.name)
9}

This keeps the persistence format compact and makes the struct easy to evolve using standard Codable techniques.

Wrap Persistence in a Small Store Type

In production code, it is better to hide the raw key names and encoding logic behind a small helper. That keeps view controllers and SwiftUI views from duplicating persistence code.

swift
1import Foundation
2
3struct User: Codable {
4    let name: String
5    let age: Int
6}
7
8enum SettingsStore {
9    private static let currentUserKey = "currentUser"
10
11    static func save(_ user: User) throws {
12        let data = try JSONEncoder().encode(user)
13        UserDefaults.standard.set(data, forKey: currentUserKey)
14    }
15
16    static func loadUser() throws -> User? {
17        guard let data = UserDefaults.standard.data(forKey: currentUserKey) else {
18            return nil
19        }
20        return try JSONDecoder().decode(User.self, from: data)
21    }
22
23    static func removeUser() {
24        UserDefaults.standard.removeObject(forKey: currentUserKey)
25    }
26}

That wrapper also gives you one place to handle migrations, fallback logic, and test injection later.

Choosing an Encoder and Planning for Changes

JSONEncoder is popular because its output is easy to inspect during debugging. PropertyListEncoder also works well and fits naturally with the property-list ecosystem. Either is fine as long as your read and write paths agree.

You should also think about versioning. If the struct changes later, older saved data may still exist on user devices. Default values, optional properties, or custom decoding logic can help keep backward compatibility manageable.

When UserDefaults Is the Wrong Tool

This pattern is appropriate for small settings-like objects such as the current profile, a selected filter, or a recent search preference. It is a poor fit for large datasets, rapidly changing collections, or sensitive credentials.

If the data is secret, use the Keychain. If the data is large or relational, use a database or files. Treat UserDefaults as a convenience store for lightweight app preferences, not as a general persistence layer.

Common Pitfalls

Trying to store the struct directly is the first mistake. Encode to Data first.

Using try? everywhere can hide data corruption or migration bugs. Surface failures where they matter.

Storing large models in UserDefaults leads to slow, messy persistence design. Keep it for small settings-oriented values.

Summary

  • A Swift struct must be encoded before it can be stored in UserDefaults.
  • 'Codable plus an encoder such as JSONEncoder is the standard approach.'
  • Save encoded Data, then decode it when reading.
  • Wrap keys and encoding logic in a helper so persistence stays centralized.

Course illustration
Course illustration

All Rights Reserved.