PropertyWrapper composition: `@Published @AppStorage("persisted") var data = 0` no parse error but error when compile

Edit: There is no need to compose @Published @AppStorage("persisted") because @AppStorage does "publish" its change and can be used inside ObservableObject and work just like @Published.

There is DefaultsWrapper package: a replacement of @AppStorage: it support many more types like Decimal, UUID. It's extensible, too!

Original post:

final class PublishedAppStorage: ObservableObject {
    @Published @AppStorage("persisted") var data = 0
}

when compile get this error in the error navigator pane:

Key path value type 'Int' cannot be converted to contextual type 'AppStorage'

same error when change to this:

final class PublishedAppStorage: ObservableObject {
    @Published<AppStorage<Int>> @AppStorage("persisted") var data = 0
}

I've never seen compile error like this in Xcode: no parse error but compile result in error in the error pane, not at the code site. (Edit: I think I understand why: the error occur inside the property wrapper, somehow the @SomePW compiler machinery cannot point the error site back at the call site)

Is there anyway to compose these two property wrappers?

My workaround:

// have to use this separate enum
enum PersistStore {
    // cannot put this in `PublishedAppStorage` and used directly in there
    @AppStorage("persisted") static var persisted = 0
}

final class PublishedAppStorage: ObservableObject {
    // this do not compile
//    @Published<AppStorage<Int>> @AppStorage("persisted") var data = 0
    @Published var data = PersistStore.persisted {
        didSet {
            PersistStore.persisted = data
        }
    }
}

These are all boilerplate, is it possible to just make a @PublishedAppStorage property wrapper have the functionality of a @Published that save change to AppStorage?, provide two projectedValue: one a Publisher, the other a SwiftUI.Binding<Value>...

Full test case
import SwiftUI

final class PublishedAppStorage: ObservableObject {
    @Published @AppStorage("persisted") var data = 0
}

struct ContentView: View {
    @EnvironmentObject var data: PublishedAppStorage

    var body: some View {
        VStack {
            Text("Hello, world! data = \(data.data)")
                .padding()
            TextField("Enter", value: $data.data, format: .number, prompt: Text("Value"))
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

You cannot compose property wrappers. In this case, using both is completely unnecessary because AppStorage already has the same functionality as Published—when the user defaults value changes, the view hierarchy will be invalidated.

The fact that the compiler gives you an incorrect error is a bug. The compiler developers obviously didn't foresee someone trying to compose property wrappers.

:raised_hands::pray: So nice it works like this!

You can for some combination. This is added by @hborla I think: How does propertyWrapper composition work vis-à-vis SwiftUI @Environment(\.presentationMode) @Binding? - #2 by hborla

Yes, property wrappers are composable. One of the few exceptions is a property wrapper that accesses the enclosing-self instance, like @Published - these wrappers don't yet work with composition. I think this is mostly a bug in the implementation, but I haven't dug too deeply into the issue yet.

Yes, as you suspected, the compiler error is actually inside the compiler-synthesized code for the property wrapper accessor, which doesn't have a valid source location in your project. In any case, this is definitely a compiler bug with the (underscored) enclosing-self subscript feature of property wrappers.

There might be a bug (or limitation with Combine publisher?): if @AppStorage is static, view doesn't get refreshed when AppStorage's value is changed:

import SwiftUI

struct ContentView: View {
    @AppStorage("number") static var number = 0
    @AppStorage("number") var alias = 0


    var body: some View {
        VStack {
            // this view never gets refresh on value change
            Text("number = \(Self.number)")
            // edit change get persisted, but view doesn't refresh
            TextField("Enter", value: Self.$number, format: .number, prompt: Text("Number"))
            // unless the view contains reference to the same @AppStorage but as instance property
            // uncomment this to see the view correctly refresh on edit change
//            Text("alias = \(alias)")
        }
    }
}

I was hoping to avoid duplicating the same @AppStorage as instance property everywhere by using a single static one. But as the test show, views don't get refresh when the value change. So I have to repeat the same as instance property all over the places.

Create an ObservableObject with an @AppStorage property and inject the observable object into the root of your view hierarchy using the environmentObject extension. Then, access the object from your views using @EnvironmentObject.

1 Like
Terms of Service

Privacy Policy

Cookie Policy