Persist data with UserDefaults in SwiftUI

Small pieces of App data can be stored using UserDefaults in SwiftUI. This is particularly good for application settings rather than user data that may be better stores in a larger database or using core data. This article describes how to use UserDefaults to store application data.



Create Polygon app

Create a new iOS app to display a regular polygon shape. This displays the shape with options to change the number of sides, change the color and set whether the shape is solid or outlined. This is implemented using the MVVM pattern as described in MVVM in SwiftUI. When this app is closed and removed from memory and relaunched, the shape is reset to the original default.

Model

 1struct PolygonModel {
 2    var sideNumber: Int
 3    var solid: Bool
 4    var colorIndex: Int
 5
 6    fileprivate var colors:[Color] = [.red, .orange, .yellow, .green, .blue, .purple]
 7
 8    init(sides: Int, solid: Bool, colorIndex: Int) {
 9        self.sideNumber = sides
10        self.solid = solid
11        self.colorIndex = colorIndex
12    }
13
14    public var ShapeColor: Color {
15        colors[colorIndex % colors.count]
16    }
17
18    mutating func nextColor() {
19        self.colorIndex += 1
20    }
21}

ViewModel

 1class PolygonViewModel: ObservableObject {
 2    @Published private var polygon = PolygonModel(
 3        sides: 3,
 4        solid: true,
 5        colorIndex: 0)
 6
 7    var sides: Double {
 8        get { Double(polygon.sideNumber) }
 9        set { polygon.sideNumber = Int(newValue) }
10    }
11
12    var solid: Bool {
13        get { polygon.solid }
14        set { polygon.solid = newValue }
15    }
16
17    var shapeColor: Color {
18        get { return polygon.ShapeColor }
19    }
20
21    func nextColor() {
22        polygon.nextColor()
23    }
24}

View

 1struct PolygonView: View {
 2    @ObservedObject var polygonVm: PolygonViewModel
 3
 4    var body: some View {
 5        VStack(spacing:40) {
 6            Text("Polygon")
 7                .font(.title)
 8
 9            ZStack {
10                RegularPolygon(sides: Int(polygonVm.sides))
11                    .fill(polygonVm.solid ? polygonVm.shapeColor : Color.clear)
12                RegularPolygon(sides: Int(polygonVm.sides))
13                    .stroke(polygonVm.shapeColor, lineWidth: 6.0)
14            }
15            .animation(.default)
16            .frame(width: 200, height: 200)
17
18            HStack(spacing:50) {
19                Spacer()
20                VStack {
21                    Text("Sides \(polygonVm.sides, specifier: "%.0F" )")
22                    Slider(value: $polygonVm.sides,
23                           in: 3...20,
24                           minimumValueLabel: Text("3"),
25                           maximumValueLabel: Text("20")) {}
26
27                }
28                Spacer()
29            }
30
31            HStack(spacing:20) {
32                Spacer()
33
34                Button(polygonVm.solid ? "Outline" : "Solid") {
35                    polygonVm.solid.toggle()
36                }
37                .buttonStyle(BlueButtonStyle())
38
39                Button("Increment Color") {
40                    polygonVm.nextColor()
41                }
42                .buttonStyle(BlueButtonStyle())
43
44                Spacer()
45            }
46
47            Spacer()
48        }
49    }
50}

Main App modified to create instance of Polygon ViewModel when application is launched.

 1@main
 2struct PolygonApp: App {
 3
 4    var polygonVm = PolygonViewModel()
 5
 6    var body: some Scene {
 7        WindowGroup {
 8            PolygonView(polygonVm: polygonVm)
 9        }
10    }
11}

Simple Polygon app implemented using MVVM
Simple Polygon app implemented using MVVM



Save Polygon Data to UserDefaults

Only the Polygon Model needs to be updated to use UserDefaults in order to persist the data in the app. The properties of PolygonModel have the didSet observer added, which is called immediately after the new value is set so the new value is set on the UserDefaults. The default initialiser is updated to register keys for the properties in UserDefaults and set the initial values for the struct.

 1struct PolygonModel {
 2    var sideNumber: Int {
 3        didSet {
 4            UserDefaults.standard.set(sideNumber, forKey: "polygon.sideNumber")
 5        }
 6    }
 7
 8    var solid: Bool {
 9        didSet {
10            UserDefaults.standard.set(solid, forKey: "polygon.solid")
11        }
12    }
13
14    var colorIndex: Int {
15        didSet {
16            UserDefaults.standard.set(colorIndex, forKey: "polygon.colorIndex")
17        }
18    }
19
20    init() {
21        UserDefaults.standard.register(defaults: [
22            "polygon.sideNumber" : 4,
23            "polygon.solid" : true,
24            "polygon.colorIndex" : 3
25        ])
26
27        sideNumber = UserDefaults.standard.integer(forKey: "polygon.sideNumber")
28        solid = UserDefaults.standard.bool(forKey: "polygon.solid")
29        colorIndex = UserDefaults.standard.integer(forKey: "polygon.colorIndex")
30    }
31
32    fileprivate var colors:[Color] = [.red, .orange, .yellow, .green, .blue, .purple]
33
34    public var ShapeColor: Color {
35        colors[colorIndex % colors.count]
36    }
37
38    mutating func nextColor() {
39        self.colorIndex += 1
40    }
41}

There is one change required in the PolygonViewModel as the creation of the PolygonModel no longer takes any parameters. The default values are now set within the model.

 1class PolygonViewModel: ObservableObject {
 2    @Published private var polygon = PolygonModel()
 3
 4    var sides: Double {
 5        get { Double(polygon.sideNumber) }
 6        set { polygon.sideNumber = Int(newValue) }
 7    }
 8
 9    var solid: Bool {
10        get { polygon.solid }
11        set { polygon.solid = newValue }
12    }
13
14    var shapeColor: Color {
15        get { return polygon.ShapeColor }
16    }
17
18    func nextColor() {
19        polygon.nextColor()
20    }
21}

Polygon data saved using UserDefaults

Polygon data saved using UserDefaults




Conclusion

It can be very convenient to store small pieces of App data using UserDefaults. This is easy to use in a SwiftUI app. The designed purpose for UserDefaults is to store data related to user preferences, however it can be used to store any small set of data. In hooks in nicely to the MVVM design pattern where only the model needs to be modified to store and retrieve model data in UserDefaults.