属性包装器和协议声明



使用Xcode 11 beta 6,我尝试使用@Published为具有属性的类型声明一个协议(但我想这个问题可以推广到任何PropertyWrapper)。

final class DefaultWelcomeViewModel: WelcomeViewModel & ObservableObject {
@Published var hasAgreedToTermsAndConditions = false
}

为此,我试图声明:

protocol WelcomeViewModel {
@Published var hasAgreedToTermsAndConditions: Bool { get }
}

这会导致编译错误:Property 'hasAgreedToTermsAndConditions' declared inside a protocol cannot have a wrapper

所以我尝试将其更改为:

protocol WelcomeViewModel {
var hasAgreedToTermsAndConditions: Published<Bool> { get }
}

并尝试

这不会编译,DefaultWelcomeViewModel does not conform to protocol,好吧,嗯,那我不能用Published<Bool>,让我们试试吧!

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)
}

这会导致Toggle上的编译错误:Cannot convert value of type 'Published<Bool>' to expected argument type 'Binding<Bool>'

问:如何使用 PropertyWrapper 为具体类型的属性创建协议属性?

我认为您提出的明确问题与您尝试解决的问题不同,但我会尽力帮助解决这两个问题。

首先,您已经意识到不能在协议中声明属性包装器。这是因为属性包装器声明在编译时合成为三个单独的属性,这不适用于抽象类型。

因此,要回答您的问题,您不能在协议中显式声明属性包装器,但您可以为属性包装器的每个合成属性创建单独的属性要求,例如:

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 }
}

如您所见,两个属性(_hasAgreed$hasAgreed)已由具体类型的属性包装器合成,我们可以简单地从协议所需的计算属性中返回这些属性。

现在我相信我们的Toggle完全有一个不同的问题,编译器很高兴地提醒我们:

无法将类型为"已发布"的值转换为预期的参数类型"绑定">

此错误也很简单。Toggle期望一个Binding<Bool>,但我们试图提供一个不同类型的Published<Bool>。幸运的是,我们选择使用@EnvironmentObject,这使我们能够使用viewModel上的"投影值"来获取视图模型属性的Binding。可以使用符合条件的属性包装器上的$前缀访问这些值。事实上,我们已经在上面用hasAgreedPublisher属性完成了此操作。

因此,让我们更新我们的Toggle以使用Binding

struct WelcomeView: View {
@EnvironmentObject var viewModel: DefaultWelcomeViewModel
var body: some View {
Toggle(isOn: $viewModel.hasAgreed) {
Text("I agree to the terms and conditions")
}
}
}

通过在viewModel前面加上$,我们可以访问一个支持视图模型"动态成员查找"的对象,以便获得视图模型成员的Binding

我会考虑另一种解决方案 - 让协议定义一个包含所有状态信息的类属性:

protocol WelcomeViewModel {
var state: WelcomeState { get }
}

WelcomeState 具有@Published属性:

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

现在,您仍然可以从视图模型实现中发布更改,但可以直接从视图中观察它:

struct WelcomeView: View {
@ObservedObject var welcomeState: WelcomeState
//....
}

最新更新