Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PropertyWrappers and protocol declaration?

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>'.

Question: How can I make a protocol property for properties in concrete types using PropertyWrappers?

like image 884
Sajjon Avatar asked Aug 26 '19 06:08

Sajjon


People also ask

What is @published Swift?

@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.

What are property protocols?

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.

What is a property wrapper?

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.

What is Wrapper in IOS?

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.


1 Answers

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.

like image 107
dalton_c Avatar answered Oct 03 '22 22:10

dalton_c