UserDefaults in SwiftUI Environment with AppStorage

Here’s an improved version of your written text:

While browsing Reddit recently, I came across a question that prompted me to start this blog. I wanted to share my expertise with others so that we can all become better developers.

As I was reviewing some code, I noticed that sensitive data such as login information should not be saved to UserDefaults. Instead, it should be stored in Keychain. However, we can use AppStorage for other types of data.

To demonstrate this, I’ve created a SampleStorage class that uses AppStorage to store and retrieve data. This class includes methods for saving and retrieving employee information in a dictionary format. Since the data is stored in a dictionary in dictionary format, we need to encode and decode it before saving to UserDefaults (line 9 and 14).

final class SampleStorage: ObservableObject {
    @AppStorage("name")
    var name: String = ""
    
    @AppStorage("employeeInformation")
    var employeeData: Data = Data()
    
    func saveEmployeeInformation(info: [[String: String]]) {
        guard let newData = try? JSONEncoder().encode(info) else { return }
        employeeData = newData
    }
    
    func employeeInformation() -> [[String: String]]? {
        guard let employeeInformation = try? JSONDecoder().decode([[String: String]].self, from: employeeData)
        else {
            return nil
        }
        return employeeInformation
        
        // Advanced: - One liner return approach -
        // return try? JSONDecoder().decode([[String: String]].self, from: employeeData)
    }
}

Here’s a sample view that demonstrates how to use the SampleStorage class to store and retrieve employee information.

import SwiftUI

struct UserDefaultSampleView: View {
    @ObservedObject
    private var storage = SampleStorage()
    
    var body: some View {
        VStack {
            Text("Name: " + storage.name)
                .padding()
            
            Text("Employee information: ")
            Text(storage.employeeInformation()?.description ?? "")
            
            Divider()
            
            updateNameButton
            updateEmployeeInformationButton
            removeEmployeeInformation
        }
    }
    
    // MARK: - Private -
    
    @ViewBuilder
    private var updateNameButton : some View {
        Button("Update name") {
            guard let name = randomName() else { return }
            storage.name = name
        }
        .padding()
    }
    
    @ViewBuilder
    private var updateEmployeeInformationButton: some View {
        Button("Update employee information") {
            guard let name = randomName(),
                  let middleName = randomName(),
                  let lastName = randomName()
            else {
                return
            }
            
            let information = [
                ["name": name],
                ["middleName": middleName],
                ["lastName": lastName]
            ]
            storage.saveEmployeeInformation(info: information)
        }
        .padding()
    }
    
    @ViewBuilder
    private var removeEmployeeInformation: some View {
        Button("Remove employee information") {
            storage.employeeData = Data()
        }
        .padding()
    }
    
    private func randomName() -> String? {
        let names = ["Zoey", "Chloe", "Amani", "Amaia", "Tom", "John"]
        guard let randomName = names.randomElement() else { return nil }
        return randomName
    }
}

Feel free to post any suggestions, questions in the comments below.

Leave a Comment