Search code examples
swiftswiftuiproperty-wrapper

Swift property wrapper value in struct not updating view


I just learned about Swift's property wrappers from Nick Sarno's YT video and they seem pretty cool. I'm investigating and I created a property wrapper to uppercase a string:

@propertyWrapper
struct Uppercased: DynamicProperty {
    @State private var value: String
    
    var wrappedValue: String {
        get {
            value
        } nonmutating set {
            value = newValue.uppercased()
        }
    }
    
    init(wrappedValue: String) {
        self.value = wrappedValue.uppercased()
    }
}

Then I can include it in a view like:

struct ContentView: View {
    @Uppercased private var str: String = "pork on the fork"
    
    var body: some View {
        VStack {
            Text(str)
            Button("New string") {
                str = "chicken on the knife"
            }
        }
        .padding()
    }
}

When I press the button the text updated.

Problem

However, when I put the @Uppercased value inside a nested strut which is then out into the main view with @State, the UI does not update, even when the structure conforms to DynamicProperty too.


Solution

  • Uppercased already has a @State. By wrapping Model in another @State, the view would be observing the "outer" @State, not the "inner" one that stores the str. The "outer" @State can't see any changes when you change the "inner" @State. It doesn't work for the same reason as why

    @State @State private var str = "Foo"
    

    doesn't work.

    Removing the outer @State and just writing

    private var model = Model()
    

    in the View would work. But it will not observe changes to the non-DynamicPropertys in Model.

    For such general-purpose property wrappers as Uppercased, I'd suggest not making it DynamicProperty.

    @propertyWrapper
    struct Uppercased {
        private var value: String
        
        var wrappedValue: String {
            get {
                value
            } set {
                value = newValue.uppercased()
            }
        }
        
        init(wrappedValue: String) {
            self.value = wrappedValue.uppercased()
        }
    }
    

    If you really want a DynamicProperty, you can create a DynamicProperty version of the above like this:

    @propertyWrapper
    struct UppercasedState: DynamicProperty {
        @State private var value: Uppercased
        
        var wrappedValue: String {
            get {
                value.wrappedValue
            } nonmutating set {
                value.wrappedValue = newValue
            }
        }
        
        init(wrappedValue: String) {
            self.value = Uppercased(wrappedValue: wrappedValue)
        }
    }
    

    Use @UppercasedState in SwiftUI structs like View/ViewModifier, and use @Uppercased everywhere else.