Using Xcode 11 beta 6, I am trying to declare a protocol for a type with properties using @Published
(but this question can be generalized to any PropertyWrapper I guess).
final class DefaultWelcomeViewModel: WelcomeViewModel & ObservableObject {
@Published var hasAgreedToTermsAndConditions = false
}
For which I try to declare:
protocol WelcomeViewModel {
@Published var hasAgreedToTermsAndConditions: Bool { get }
}
Which results in a compilation error: Property 'hasAgreedToTermsAndConditions' declared inside a protocol cannot have a wrapper
So I try to change it into:
protocol WelcomeViewModel {
var hasAgreedToTermsAndConditions: Published<Bool> { get }
}
And trying
Which does not compile, DefaultWelcomeViewModel does not conform to protocol
, okay, so hmm, I cannot using Published<Bool>
then, let's try it!
struct WelcomeScreen<ViewModel> where ViewModel: WelcomeViewModel & ObservableObject {
@EnvironmentObject private var viewModel: ViewModel
var body: some View {
// Compilation error: `Cannot convert value of type 'Published<Bool>' to expected argument type 'Binding<Bool>'`
Toggle(isOn: viewModel.hasAgreedToTermsAndConditions) {
Text("I agree to the terms and conditions")
}
}
}
// MARK: - ViewModel
protocol WelcomeViewModel {
var hasAgreedToTermsAndConditions: Published<Bool> { get }
}
final class DefaultWelcomeViewModel: WelcomeViewModel & ObservableObject {
var hasAgreedToTermsAndConditions = Published<Bool>(initialValue: false)
}
Which results in the compilation error on the Toggle
: Cannot convert value of type 'Published<Bool>' to expected argument type 'Binding<Bool>'
.
@Published is one of the property wrappers in SwiftUI that allows us to trigger a view redraw whenever changes occur. You can use the wrapper combined with the ObservableObject protocol, but you can also use it within regular classes.
It aims to ensure that neighbours exchange sufficient information in a timely manner to minimise the scope for disputes between them; and to enable any such disputes to be readily resolved, including by alternative disputes resolution (ADR), keeping costs to a minimum.
Property Wrappers. A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property.
A property wrapper is a generic structure that encapsulates read and write access to the property and adds additional behavior to it. We use it if we need to constrain the available property values, add extra logic to the read/write access (like using databases or user defaults), or add some additional methods.
I think the explicit question you're asking is different from the problem you are trying to solve, but I'll try to help with both.
First, you've already realized you cannot declare a property wrapper inside a protocol. This is because property wrapper declarations get synthesized into three separate properties at compile-time, and this would not be appropriate for an abstract type.
So to answer your question, you cannot explicitly declare a property wrapper inside of a protocol, but you can create individual property requirements for each of the synthesized properties of a property wrapper, for example:
protocol WelcomeViewModel {
var hasAgreed: Bool { get }
var hasAgreedPublished: Published<Bool> { get }
var hasAgreedPublisher: Published<Bool>.Publisher { get }
}
final class DefaultWelcomeViewModel: ObservableObject, WelcomeViewModel {
@Published var hasAgreed: Bool = false
var hasAgreedPublished: Published<Bool> { _hasAgreed }
var hasAgreedPublisher: Published<Bool>.Publisher { $hasAgreed }
}
As you can see, two properties (_hasAgreed
and $hasAgreed
) have been synthesized by the property wrapper on the concrete type, and we can simply return these from computed properties required by our protocol.
Now I believe we have a different problem entirely with our Toggle
which the compiler is happily alerting us to:
Cannot convert value of type 'Published' to expected argument type 'Binding'
This error is straightforward as well. Toggle
expects a Binding<Bool>
, but we are trying to provide a Published<Bool>
which is not the same type. Fortunately, we have chosen to use an @EnvironmentObject
, and this enables us to use the "projected value" on our viewModel
to obtain a Binding
to a property of the view model. These values are accessed using the $
prefix on an eligible property wrapper. Indeed, we have already done this above with the hasAgreedPublisher
property.
So let's update our Toggle
to use a Binding
:
struct WelcomeView: View {
@EnvironmentObject var viewModel: DefaultWelcomeViewModel
var body: some View {
Toggle(isOn: $viewModel.hasAgreed) {
Text("I agree to the terms and conditions")
}
}
}
By prefixing viewModel
with $
, we get access to an object that supports "dynamic member lookup" on our view model in order to obtain a Binding
to a member of the view model.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With