Skip to content

Instantly share code, notes, and snippets.

@ericlewis
Last active October 15, 2024 21:40
Show Gist options
  • Save ericlewis/37fcd63cd04e03afe8e01f914e52d1fd to your computer and use it in GitHub Desktop.
Save ericlewis/37fcd63cd04e03afe8e01f914e52d1fd to your computer and use it in GitHub Desktop.
Using Codable with AppStorage, the easy way!
import Foundation
import SwiftUI
// MARK: Demo
struct Example: RawRepresentable, Codable {
let id: UUID
}
// This is functionally the same as above, but we have a neater type without creating a new protocol.
typealias RawCodable = RawRepresentable & Codable
struct Example2: RawCodable {
let id: UUID
}
struct ContentView: View {
@AppStorage("1")
var first = [1]
@AppStorage("2")
var second = ["test": 123]
@AppStorage("3")
var third = Example(id: .init())
var body: some View {
Text(first.description)
Text(second.description)
Text(third.id.description)
}
}
// MARK: General Codables
extension RawRepresentable where Self: Codable {
public init?(rawValue: String) {
guard let value: Self = Coders.encode(rawValue) else { return nil }
self = value
}
public var rawValue: String {
Coders.decode(self, default: "{}")
}
}
// MARK: Collections
// Slightly more complex than structs.
extension Collection where Self: RawRepresentable, Self: Codable, Element: Codable {
public var rawValue: String {
Coders.decode(self, default: "[]")
}
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let value: Self = Coders.encode(rawValue) else { return nil }
self = value
}
}
extension Set: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let value: Self = Coders.encode(rawValue) else { return nil }
self = value
}
}
// MARK: Dictionary
extension Dictionary: RawRepresentable where Key: Codable, Value: Codable {
public init?(rawValue: String) {
guard let value: Self = Coders.encode(rawValue) else { return nil }
self = value
}
public var rawValue: String {
Coders.decode(self, default: "{}")
}
}
// MARK: Private Helpers
fileprivate struct Coders {
private static var _decoder: JSONDecoder { JSONDecoder() }
private static var _encoder: JSONEncoder { JSONEncoder() }
fileprivate static func encode<Value: Codable>(_ value: String) -> Value? {
guard let data = value.data(using: .utf8), let decoded = try? _decoder.decode(Value.self, from: data) else {
return nil
}
return decoded
}
fileprivate static func decode<Value: Codable>(_ value: Value, default: String) -> String {
guard let data = try? _encoder.encode(value), let value = String(data: data, encoding: .utf8) else {
return `default`
}
return value
}
}
@ericlewis
Copy link
Author

Bonus: skip all the raw representable stuff & just work with Codable types directly:

@propertyWrapper
public struct CodableAppStorage<Value: Codable>: DynamicProperty {
    @AppStorage
    private var value: Data
    
    private let decoder = JSONDecoder()
    private let encoder = JSONEncoder()
    
    public init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) {
        _value = .init(wrappedValue: try! encoder.encode(wrappedValue), key, store: store)
    }
    
    public var wrappedValue: Value {
        get { try! decoder.decode(Value.self, from: value) }
        nonmutating set { value = try! encoder.encode(newValue) }
    }
    
    public var projectedValue: Binding<Value> {
        Binding(
            get: { wrappedValue }, 
            set: { wrappedValue = $0 }
        )
    }
}

This code is obviously unsafe with all the force unwrapping, but it can be adjusted as needed!

@lourensm
Copy link

lourensm commented Sep 1, 2024

Just saw your CodableAppStorage propertyWrapper solution. It looks elegant and practical to me:
No extensions of builtin types and no copy/paste of code. Why should one use the CodableAppStorage.swift solution then?
Thanks, Lourens

@josephlevy222
Copy link

Back in January 2024 I wrote almost identical code using the propertyWrapper as above. Wish I had done a better search then. Now I'm trying to get the key to be the variable name (plus maybe a namespace identifier) but haven't figured out Swift macros enough yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment