简体   繁体   中英

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.

This works when using MyViewModel like this:

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

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:

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.

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.

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.

I don't fully understand why this works, but it appears that if the protocol extends ObservableObject, then the @ObservedObject dynamic member lookup works:

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

Alternatively, you could manually implement the magic behind the @ObservedObject and @StateObject property wrappers. 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:

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

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. Hopefully future updates to Swift, such as this proposal , will provide more compatibility.

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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