Thank you for creating this.
It's missing one functionality of @AppStorage
: its a published so views are notified and updated when it change.
Here is my small test, note the name
field is an @AppStorage
, when you edit it, all the views update on each edit change. But the date
and decimal
views do not update when you edit them.
I am not sure why your's is not working as you use a @AppStorage
, it should publish when it change.
Anyway, DateAppStorage
and DecimalAppStorage
need this publish functionality to be equivalent to @AppStorage
. I don't know how to make this happen.
There is this drop in replacement of @AppStorage
, it supports many more different types, including Date
and Decimal
and can be easily extended to support new type: GitHub - Pyroh/DefaultsWrapper: Yet another property wrapper for UserDefaults. It works just like @AppStorage
. I try to see how he does the publish part but cannot understand 
import SwiftUI
final class AppData: ObservableObject {
@DateAppStorage("date") var date = Date() // change not published
@DecimalAppStorage("decimal") var decimal = Decimal.zero // change not published
@AppStorage("name") var name = "Paul" // change to this is published!
}
struct Child: View {
@EnvironmentObject private var appData: AppData
var body: some View {
VStack {
Text(appData.date, format: .iso8601) // not updated
Text(appData.decimal, format: .number) // not updated
Text(appData.name) // yes updated!
}
.background(Color.indigo)
}
}
struct ContentView: View {
@StateObject private var appData = AppData()
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Text(appData.date, format: .iso8601) // not updated as it's editted
DatePicker("Date", selection: $appData.date)
Text(appData.decimal, format: .number) // not updated as it's editted
TextField("Enter", value: $appData.decimal, format: .number, prompt: Text("Decimal"))
Text(appData.name) // @AppStorage is updated as it's editted
TextField("Name", text: $appData.name)
Divider()
Child()
}
.environmentObject(appData)
}
}
// ======================================================================
@propertyWrapper
struct DateAppStorage: DynamicProperty {
@AppStorage var storage: Double
init(wrappedValue: Date, _ key: String, store: UserDefaults? = nil) {
_storage = AppStorage(wrappedValue: wrappedValue.timeIntervalSinceReferenceDate, key, store: store)
}
var wrappedValue: Date {
get {
return Date(timeIntervalSinceReferenceDate: storage)
}
nonmutating set {
storage = newValue.timeIntervalSinceReferenceDate
}
}
var projectedValue: Binding<Date> {
return Binding(
get: { wrappedValue },
set: { wrappedValue = $0 })
}
}
@propertyWrapper
struct DecimalAppStorage: DynamicProperty {
@AppStorage var storage: Data
init(wrappedValue: Decimal, _ key: String, store: UserDefaults? = nil) {
var data = Data(capacity: 36)
Self.writeData(from: wrappedValue, into: &data)
_storage = AppStorage(wrappedValue: data, key, store: store)
}
var wrappedValue: Decimal {
get {
return Self.decimal(from: storage)
}
nonmutating set {
Self.writeData(from: newValue, into: &storage)
}
}
var projectedValue: Binding<Decimal> {
return Binding(
get: { wrappedValue },
set: { wrappedValue = $0 })
}
static func writeData(from decimal: Decimal, into data: inout Data) {
data.removeAll(keepingCapacity: true)
func write<I: FixedWidthInteger>(trivialInteger: I, into data: inout Data) {
withUnsafePointer(to: trivialInteger.littleEndian) {
data.append(UnsafeBufferPointer(start: $0, count: 1))
}
}
write(trivialInteger: decimal._exponent, into: &data)
write(trivialInteger: decimal._length, into: &data)
write(trivialInteger: decimal._isNegative, into: &data)
write(trivialInteger: decimal._isCompact, into: &data)
write(trivialInteger: decimal._reserved, into: &data)
withUnsafePointer(to: decimal._mantissa) {
let contents = UnsafeBufferPointer(
start: UnsafeRawPointer($0).assumingMemoryBound(to: UInt16.self),
count: 8)
for instance in contents {
write(trivialInteger: instance, into: &data)
}
}
}
static func decimal(from data: Data) -> Decimal {
return data.withUnsafeBytes {
guard $0.count == 36, var base = $0.baseAddress else {
// handle the error
print("Unexpected data count; defaulting to zero.")
return Decimal()
}
/// - Warning: `T.bitWidth` must be a multiple of 8.
func read<T: FixedWidthInteger>(as type: T.Type, from pointer: inout UnsafeRawPointer) -> T {
var integer = T()
// There are no guarantees about the alignment of `Data`'s buffer, so we shouldn't
// use `pointer.load(as: T.self)`
for shift in stride(from: 0, to: T.bitWidth, by: 8) {
integer |= T(truncatingIfNeeded: pointer.load(as: UInt8.self)) << shift
pointer += 1
}
return integer
}
let exponent = read(as: Int32 .self, from: &base)
let length = read(as: UInt32.self, from: &base)
let isNegative = read(as: UInt32.self, from: &base)
let isCompact = read(as: UInt32.self, from: &base)
let reserved = read(as: UInt32.self, from: &base)
let mantissa = (
read(as: UInt16.self, from: &base),
read(as: UInt16.self, from: &base),
read(as: UInt16.self, from: &base),
read(as: UInt16.self, from: &base),
read(as: UInt16.self, from: &base),
read(as: UInt16.self, from: &base),
read(as: UInt16.self, from: &base),
read(as: UInt16.self, from: &base))
return Decimal(
_exponent: exponent,
_length: length,
_isNegative: isNegative,
_isCompact: isCompact,
_reserved: reserved,
_mantissa: mantissa)
}
}
}