21

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?

Sajjon
  • 8,938
  • 5
  • 60
  • 94
  • Read my question again :) – Sajjon Aug 26 '19 at 06:59
  • 1
    The closest I got to it, was this: https://stackoverflow.com/a/57527336/7786555 – kontiki Aug 26 '19 at 07:39
  • This question should probably be flagged as a duplicate of that question. But question is giving a little bit more context. Too bad that we did not really "get there", so that does not compile? – Sajjon Aug 26 '19 at 07:42
  • Yes, I though of closing as duplicate but did not want your comments to be "lost". Maybe if you move them as a reply to the other question, we can close this one. Anyway, I did try different access types, but maybe I am missing something. Please give it a try yourself and post your results (if any ;-). Cheers. – kontiki Aug 26 '19 at 08:27
  • A fix for this is being discussed here: https://forums.swift.org/t/property-wrapper-requirements-in-protocols/33953 – Sajjon Mar 10 '20 at 11:27

2 Answers2

38

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.

dalton_c
  • 6,876
  • 1
  • 33
  • 42
0

I would consider another solution - have the protocol define a class property with all your state information:

protocol WelcomeViewModel {
    var state: WelcomeState { get }
}

And the WelcomeState has the @Published property:

class WelcomeState: ObservableObject {
    @Published var hasAgreedToTermsAndConditions = false
}

Now you can still publish changes from within the viewmodel implementation, but you can directly observe it from the view:

struct WelcomeView: View {
    @ObservedObject var welcomeState: WelcomeState

    //....
}
Greg Ennis
  • 14,917
  • 2
  • 69
  • 74
  • I don't think this is what OP was asking for. The question was how to insure that the `@Published` requirement is in the protocol contract so that if the conforming type doesn't include `@Published`, a compiler error would be emitted. In your answer, the conforming type can easily omit adding the `@ObservedObject` property wrapper and the project would build successfully. – alobaili Sep 07 '22 at 21:24
  • @alobaili I don't agree, anyone implementing this protocol must provide the object WelcomeState, that does have `@Published` property. Since what the OP is asking for is not possible, there are different approaches to get the same result. I have posted another one since this question does not have an accepted answer. – Greg Ennis Sep 07 '22 at 23:50