🚀

Using SwiftUI's @AppStorage with Arrays, Dates, and Custom Types

(Updated on )

TL;DR: By default, @AppStorage does not support Date or Array. However, by making your custom types (or extending system types) conform to the RawRepresentable protocol and mapping their RawValue to a String (via JSON), you can easily bypass this limitation.

Background

@AppStorage is a property wrapper provided by SwiftUI that serves as an elegant wrapper around UserDefaults. However, out of the box, it only supports basic types such as Bool, Int, Double, String, URL, and Data. When developers attempt to store a Date, an Array ([String]), or a custom struct, the compiler will throw an error.

The Solution

While @AppStorage doesn’t directly support complex types, it does support any type that conforms to RawRepresentable, provided the RawValue is an Int or String. We can leverage JSON encoding/decoding to convert complex objects into a String, effectively “tricking” @AppStorage into handling them.

Below is the implementation strategy tailored for the Swift 6 environment (noting the use of @retroactive).

1. Supporting Date

We extend Date to encode itself as a JSON string.

Swift
// Swift 6: Use @retroactive to silence retroactive conformance warnings
extension Date: @retroactive RawRepresentable {
    public typealias RawValue = String
    
    public init?(rawValue: RawValue) {
        guard let data = rawValue.data(using: .utf8),
              let date = try? JSONDecoder().decode(Date.self, from: data) else {
            return nil
        }
        self = date
    }

    public var rawValue: RawValue {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8) else {
            return ""
        }
        return result
    }
}

// Usage
struct DateView: View {
    @AppStorage("lastLogin") var lastLogin = Date()
    // ...
}

2. Supporting Arrays

By using a generic extension, we can enable @AppStorage support for any Array whose elements are Codable.

Swift
// Swift 6: Use @retroactive to make intent clear
extension Array: @retroactive RawRepresentable where Element: Codable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode([Element].self, from: data) else {
            return nil
        }
        self = result
    }

    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8) else {
            return "[]"
        }
        return result
    }
}

// Usage
struct ListView: View {
    @AppStorage("savedIds") var ids = [1, 2, 3]
    @AppStorage("history") var history: [String] = []
    // ...
}

3. Supporting Custom Structs

For custom structures, you don’t need an extension. simply declare conformance to RawRepresentable within the type definition.

Swift
struct UserSettings: Codable, RawRepresentable {
    var isDarkMode: Bool
    var username: String
    
    // Boilerplate: Implement RawRepresentable to convert self to String
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode(UserSettings.self, from: data) else {
            return nil
        }
        self = result
    }

    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8) else {
            return "{}"
        }
        return result
    }
}

// Usage
@AppStorage("settings") var settings = UserSettings(isDarkMode: false, username: "Guest")

Key Considerations

  1. Performance Overhead: Every read and write triggers JSON encoding/decoding. Be cautious when storing large arrays, as @AppStorage updates often occur on the main thread, potentially causing UI hitches.
  2. Swift 6 Compatibility: In Swift 6, retroactive conformance (extending a type you don’t own to conform to a protocol you don’t own) generates a warning. Use the @retroactive keyword (as shown above) to explicitly declare this behavior, or wrap the data in a custom container struct to avoid global namespace pollution.
  3. Default Values: If JSON decoding fails (e.g., corrupted data), @AppStorage will gracefully fall back to the default value you provided in the property declaration.

Further Reading

Related Tips

Subscribe to Fatbobman

Weekly Swift & SwiftUI highlights. Join developers.

Subscribe Now