简体   繁体   English

SwiftUI:将@Binding 与来自协议的@Property 变量一起使用

[英]SwiftUI: Using @Binding with @Property variable from a protocol

I have a ParentView from which I want to pass @Published variable to a Subview, where it will be used as @Bindable.我有一个 ParentView,我想从中将 @Published 变量传递给一个 Subview,它将在其中用作 @Bindable。

This works when using MyViewModel like this:这在像这样使用 MyViewModel 时有效:

class MyViewModel: ObservableObject {
    @Published var soundOn = true
}

struct ParentView: View {
    @ObservedObject var myViewModel: MyViewModel
    var body: some View {
        Subview(soundOn: $myViewModel.soundOn)
    }
}

struct Subview: View {
    @Binding var soundOn: Bool
    var body: some View {
        Image(soundOn ? "soundOn" : "soundOff")
    }
}

but I want to reuse Subview for all ViewModels conforming to the HasSoundOnOff protocol.但我想为所有符合 HasSoundOnOff 协议的 ViewModel 重用 Subview。 When using the HasSoundOnOff protocol I can't define @Published inside the protocol and this means ParentView only sees a normal non-@Published variable and can't use $viewModel.soundOn.使用 HasSoundOnOff 协议时,我无法在协议内定义 @Published,这意味着 ParentView 只能看到一个普通的非@Published 变量,不能使用 $viewModel.soundOn。

protocol HasSoundOnOff {
    var soundOn: Bool { get set }
}

class MyViewModel: HasSoundOnOff {
    @Published var soundOn = true
}

struct ParentView<ViewModel: ObservableObject & HasSoundOnOff>: View {
    @ObservedObject var viewModel: ViewModel
    var body: some View {
        Subview(soundOn: $viewModel.soundOn) //<----- error: "Expression type 'Binding<_>' is ambiguous without more context" because protocols can't have @Published and therefor soundOn is treated like a non-@Published variable
    }
}

I can let MyViewModel inherit from a class that defines the @Published variable, so the following code works:我可以让 MyViewModel 从定义 @Published 变量的 class 继承,所以下面的代码有效:

class InheritFromPublishedVarClass: ObservableObject {
    @Published var soundOn = true
}

class MyViewModel: ObservableObject & InheritFromPublishedVarClass {}

struct ParentView<ViewModel: ObservableObject & InheritFromPublishedVarClass>: View {
    @ObservedObject var viewModel: ViewModel
    var body: some View {

        Subview(soundOn: $viewModel.soundOn)
    }
}

This means I can reuse my @Published variable, but this won't scale since multiple inheritance is not allowed.这意味着我可以重用我的@Published 变量,但这不会扩展,因为不允许使用多个 inheritance。

This is seriously limiting code reusability for me.这严重限制了我的代码可重用性。 There must be a way to achieve this in a more scalable way.必须有一种方法以更具可扩展性的方式实现这一点。 Any ideas?有任何想法吗? A requirement is to have the ParentView take in a Generic ViewModel parameter.要求是让 ParentView 接受通用 ViewModel 参数。

Could you not use你不能用吗

@EnvironmentObject var myViewModel: MyViewModel

in your views where you need access to it.在您需要访问它的视图中。 Then use it as然后将其用作

myViewModel.soundOn 

you won't need to pass it across Views as Bindings, as you will have access to soundOn in any view that you declare your @EnvironmentObject in.您不需要将它作为绑定跨视图传递,因为您可以在声明 @EnvironmentObject 的任何视图中访问 soundOn。

I don't fully understand why this works, but it appears that if the protocol extends ObservableObject, then the @ObservedObject dynamic member lookup works:我不完全理解为什么会这样,但似乎如果协议扩展了 ObservableObject,那么@ObservedObject动态成员查找工作:

protocol HasSoundOnOff: ObservableObject {
    var soundOn: Bool { get set }
}

Alternatively, you could manually implement the magic behind the @ObservedObject and @StateObject property wrappers.或者,您可以手动实现@ObservedObject@StateObject属性包装器背后的魔法。 Specifically, you can tweak your protocol definition to return a Binding, and then you can manually instantiate a Binding.具体来说,您可以调整您的协议定义以返回一个绑定,然后您可以手动实例化一个绑定。

First, add a corresponding Binding parameter to your protocol:首先,将相应的绑定参数添加到您的协议中:

protocol HasSoundOnOff {
    var soundOn: Bool { get set }
    var soundOnBinding: Binding<Bool> { get }
}

Second, manually instantiate a Binding:其次,手动实例化一个 Binding:

class MyViewModel: ObservableObject, HasSoundOnOff {
    @Published var soundOn = true
    var soundOnBinding: Binding<Bool> {
        Binding(get: { self.soundOn }, set: { self.soundOn = $0 })
    }
}

Finally, use your Binding (rather than that of @ObservedObject ):最后,使用您的 Binding (而不是@ObservedObject ):

struct ParentView<ViewModel: ObservableObject & HasSoundOnOff>: View {
    @ObservedObject var viewModel: ViewModel
    var body: some View {
        Subview(soundOn: viewModel.soundOnBinding)
    }
}

SwiftUI's property wrappers just don't seem to play very well with protocol inheritance. SwiftUI 的属性包装器似乎不太适合协议继承。 Hopefully future updates to Swift, such as this proposal , will provide more compatibility.希望未来对 Swift 的更新,比如这个提案,将提供更多的兼容性。

You could use a protocol extension for this.您可以为此使用协议扩展。 Note that the protocol also has to conform to ObservableObject but in your use case this should be fine.请注意,该协议还必须符合ObservableObject但在您的用例中这应该没问题。

protocol HasSoundOnOff: ObservableObject {
    var soundOn: Bool { get set }
}

extension HasSoundOnOff {
    var soundOnBinding: Binding<Bool> {
        Binding(get: { self.soundOn }, set: { self.soundOn = $0 })
    }
}

class MyViewModel: HasSoundOnOff {
    @Published var soundOn = true
}

From here, you just need to reference the binding wrapper in the protocol extension.从这里开始,您只需要在协议扩展中引用绑定包装器。 Meanwhile, you just need to confirm to the actual protocol in your implementation.同时,你只需要在你的实现中确认实际的协议。

struct ParentView<ViewModel: HasSoundOnOff>: View {
    @ObservedObject var viewModel: ViewModel
    var body: some View {
        Subview(soundOn: viewModel.soundOnBinding)
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM