iOS
NSUserDefaults
Swift
custom objects
data storage

Save custom objects into NSUserDefaults

Master System Design with Codemia

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

Introduction

NSUserDefaults, now exposed in Swift as UserDefaults, only stores property-list types directly, such as strings, numbers, booleans, arrays, dictionaries, and Data. To save a custom object, you first encode it into Data, then write that data to defaults. For modern Swift code, Codable is the simplest and safest default approach.

Save a Custom Object with Codable

If your type is a plain Swift struct or class with codable properties, use JSONEncoder or PropertyListEncoder to turn it into Data.

swift
1import Foundation
2
3struct UserProfile: Codable {
4    let id: Int
5    let displayName: String
6    let notificationsEnabled: Bool
7}
8
9let profile = UserProfile(
10    id: 42,
11    displayName: "Mark",
12    notificationsEnabled: true
13)
14
15let encoder = JSONEncoder()
16let data = try encoder.encode(profile)
17
18UserDefaults.standard.set(data, forKey: "userProfile")

To read it back:

swift
1import Foundation
2
3if let data = UserDefaults.standard.data(forKey: "userProfile") {
4    let decoder = JSONDecoder()
5    let profile = try decoder.decode(UserProfile.self, from: data)
6    print(profile.displayName)
7}

This keeps the object model explicit and avoids older Objective-C archiving APIs unless you really need them.

When to Use NSSecureCoding

If you are working with legacy Objective-C types or a class hierarchy based on NSObject, NSSecureCoding is still relevant. In that case, archive the object into Data and store the data in defaults.

swift
1import Foundation
2
3final class Settings: NSObject, NSSecureCoding {
4    static var supportsSecureCoding: Bool { true }
5
6    let theme: String
7
8    init(theme: String) {
9        self.theme = theme
10    }
11
12    required init?(coder: NSCoder) {
13        guard let theme = coder.decodeObject(of: NSString.self, forKey: "theme") as String? else {
14            return nil
15        }
16        self.theme = theme
17    }
18
19    func encode(with coder: NSCoder) {
20        coder.encode(theme as NSString, forKey: "theme")
21    }
22}
23
24let settings = Settings(theme: "dark")
25let data = try NSKeyedArchiver.archivedData(withRootObject: settings, requiringSecureCoding: true)
26UserDefaults.standard.set(data, forKey: "settings")

And to decode:

swift
1if let data = UserDefaults.standard.data(forKey: "settings") {
2    let settings = try NSKeyedUnarchiver.unarchivedObject(ofClass: Settings.self, from: data)
3    print(settings?.theme ?? "missing")
4}

Use this pattern when you need compatibility with existing archived objects or Objective-C APIs that already depend on keyed archiving.

Know When UserDefaults Is the Wrong Storage

UserDefaults is designed for small preference-like values, not large object graphs or sensitive records. Encoding a tiny profile or settings object is fine. Storing large caches, images, or complex relational data is not.

If the data is sensitive, prefer the Keychain. If the data is large or relational, use files, SQLite, Core Data, or another persistence layer.

Updating Stored Objects Safely

A practical pattern is to wrap the save and load logic in one place so the rest of the app does not manipulate raw keys.

swift
1import Foundation
2
3enum DefaultsStore {
4    private static let profileKey = "userProfile"
5
6    static func save(_ profile: UserProfile) throws {
7        let data = try JSONEncoder().encode(profile)
8        UserDefaults.standard.set(data, forKey: profileKey)
9    }
10
11    static func loadProfile() throws -> UserProfile? {
12        guard let data = UserDefaults.standard.data(forKey: profileKey) else {
13            return nil
14        }
15        return try JSONDecoder().decode(UserProfile.self, from: data)
16    }
17}

This reduces duplicated keys and makes migrations easier if the stored format changes later.

Common Pitfalls

One common mistake is trying to store a custom object directly with set(_:forKey:). That only works for property-list-compatible values unless you encode the object first.

Another mistake is treating UserDefaults like a database. It is not optimized for large payloads or complex querying.

Developers also sometimes use NSKeyedArchiver without secure coding enabled. For modern code, prefer Codable when possible and NSSecureCoding when archiving is required.

Finally, changing a model’s properties over time can break decoding if you do not plan for migration. If the schema may evolve, make new fields optional or provide custom decoding logic.

Summary

  • Store custom objects in UserDefaults by encoding them to Data first.
  • 'Codable is the best default approach for modern Swift types.'
  • Use NSSecureCoding for legacy Objective-C classes or existing archived data.
  • Keep UserDefaults for small preference-like objects, not large or sensitive datasets.
  • Centralizing save and load logic makes keys, migrations, and testing much easier.

Course illustration
Course illustration