Skip to content

Instantly share code, notes, and snippets.

@ericlewis
Last active February 2, 2024 07:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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!

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