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.
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! :-)