Question about nested property wrappers and mutability

I have a question about nested property wrappers and mutability.

In a SwiftUI test I have a type (FirebaseValue) that is similar to a CurrentValueSubject - or a BehaviorRelay in RxSwift. In other words, it is a value that can change over time, but always has a current value.
My type is a class, and I have made it conform to BindableObject.
I have then made the type into a property wrapper.

This means that in SwiftUI, I can declare a type:

@ObjectBinding 
@FirebaseValue(...)
var configuration: Configuration

This type works perfectly for making SwiftUI subscribe to changes in my value.
But I have an issue when trying to update the value.
For instance I can create a Button action as follows:

struct ContentView : View {

    @ObjectBinding
    @FirebaseValue(path: Path.chatrooms.child("firechat").configuration)
    var configuration: Configuration = .default

    var body: some View {
        VStack {
            Text("Chatroom name: \(configuration.name)")
            Button("Tap me") {
                self.configuration = Configuration(name: "ski")
            }
        }
    }
}

This fails compiling in the button action closure with the message: "Cannot assign to property: 'self' is immutable"

So that is basically correct, but I would assume that the synthesized setter of the configuration property would be nonmutating since the property wrapped by @ObjectBinding is a class and hence it should be nonmutating as far as I can gather from the proposal.

Is this an error in the current implementation in Xcode 11 beta 3, or am I misunderstanding how the (nested) property wrapping deals with mutability?

Well configuration is just a computed get set property and the error message is correct. With the latest snapshot you should be able to workaround your issue by rewriting the mutating code to:

self._configuration.wrappedValue.wrappedValue = Configuration(name: "ski")

Here you explicitly mutating the value on a reference type (it will capture the reference from the closure), which should make the error message go away.

1 Like

The implementation is behaving correctly here. ObjectBinding is a struct and the setter for its wrappedValue is mutating, so the setter for configuration is mutating. That the property wrapped by @ObjectBinding is a class doesn't actually matter here, because the wrappedValue setter could be modifying state of the ObjectBinding struct instance itself.

Doug

1 Like

Apologies, I was writing a reply to @Douglas_Gregor on the recently closed thread, so I will continue my reply here:

Thanks for the reply. I can see that it behaves as suggested in the proposal - and I can almost see the logic, but not quite.

There is still a step I have a hard time grasping.
I have tried manually 'synthesizing' the setters using the proposal. I hope I did it right:

    private var __configuration: ObjectBinding<FirebaseValue<Configuration>> = ObjectBinding(initialValue: FirebaseValue<Configuration>(initialValue: .default, path: Path.chatrooms.child("firechat").configuration))

    private var _configuration: FirebaseValue<Configuration> {
        get { __configuration.wrappedValue }
        set { __configuration.wrappedValue = newValue }
    }

    var configuration: Configuration {
        get { _configuration.wrappedValue }
        set { _configuration.wrappedValue = newValue }
    }

In this example I can add nonmutating to the setter of the configuration just fine.
This setter is just using the 'getter' of the ObjectBinding (which is not mutating) and then calling the setter of my own class based FirebaseValue, right?
So in this case it seems that it could be possible to infer that the nonmutating would be ok.

Or am I turning all this upside down?

Your interpretation of the synthetization isn‘t correct.

You only will get a computed get set property called configuration and the backing storage named _configuration which will have the type ObjectBinding<FirebaseValue<Configuration>>.

The configuration property will then use self._configuration.wrappedValue.wrappedValue to read write to the deeply nested property.

private var _configuration: ObjectBinding<FirebaseValue<Configuration>> = ObjectBinding(
  wrappedValue: FirebaseValue<Configuration>(
    wrappedValue: .default, 
    path: Path.chatrooms.child("firechat").configuration
  )
)

var configuration: Configuration {
  get { _configuration.wrappedValue.wrappedValue }
  set { _configuration.wrappedValue.wrappedValue = newValue }
}
1 Like

Ah, I see - thanks for the correction.
But I believe that my point is still intact with this synthesis:

The synthesized setter only calls the 'getter' of the wrappedValue of ObjectBinding (which is not mutating) and the 'setter' of the wrappedValue in my own class based FirebaseValue. In this chain there is nothing mutating, so in a 'manual' synthesis I can safely add 'nonmutating' to the setter.

If my analysis does not have other flaws, shouldn't the 'mutating / nonmutating' state of the synthesized setter be calculated from the 'setter' of the innermost wrapper and the 'getter' of all outer wrappers?
I.e. if none of the outer wrappers have mutating getters and the innermost wrapper has a nonmutating setter (or if it's a class), then the synthesized setter can be nonmutating too, right?

I don‘t have a mac at hand atm. but according to the docs, the setter is not marked as nonmutating.

https://developer.apple.com/documentation/swiftui/objectbinding/3337201-wrappedvalue


@John_McCall can you split this discussion into a new #swift-users thread maybe? IIRC admins have that power.

It should be based on whether mutating _storage.wrappedValue.wrappedValue requires _storage to be mutated, which, yes, means that if either _storage or _storage.wrappedValue is a reference type then the synthesized property can be nonmutating set.

1 Like

Sure, I'll splice this and Morten's post in the old thread into a new thread.

1 Like

Thank you so much for your reply.

Do you think that I should create a bug report for the current behavior in Xcode 11 beta 3?

Do I understand you correctly, since the first wrappedValue is a reference wrapper type, it should be safe to apply the nonmutating keyword onto the setter of the computed wrapped property?

Assuming that the access for reading that storage isn't mutating, yes.

1 Like

@John_McCall do you think that I should create an issue in the bug tracker for this? Or do you think that it will naturally be part of the final implementation?

1 Like

It's worth creating an issue for it, even if it just gets fixed pretty quickly.

2 Likes

Thanks - I have created issue SR-11138

I have added my own 'algorithm' for when the synthesized getter and setter should be mutating and nonmutating respectively.
I hope that this algorithm is not horribly wrong and that it can be of use. :slight_smile:

I forgot to mention: This issue was fixed and the fix is merged into the master branch of Swift:
https://github.com/apple/swift/pull/26326

Looking forward to seeing if it will be available in a future Xcode 11 beta! :-)