如何定义协议以包含带有 @Published 属性包装器的属性
Posted
技术标签:
【中文标题】如何定义协议以包含带有 @Published 属性包装器的属性【英文标题】:How to define a protocol to include a property with @Published property wrapper 【发布时间】:2019-12-22 08:19:00 【问题描述】:当按照当前的 SwiftUI 语法使用 @Published 属性包装器时,似乎很难定义一个包含 @Published 属性的协议,或者我肯定需要帮助 :)
当我在 View 和它的 ViewModel 之间实现依赖注入时,我需要定义一个 ViewModelProtocol 以便注入模拟数据以轻松预览。
这是我第一次尝试,
protocol PersonViewModelProtocol
@Published var person: Person
我得到“在协议中声明的属性‘人’不能有包装器”。
然后我尝试了这个,
protocol PersonViewModelProtocol
var $person: Published
显然没有用,因为 '$' 是保留的。
我希望有一种方法可以在 View 和它的 ViewModel 之间放置一个协议,并利用优雅的 @Published 语法。非常感谢。
【问题讨论】:
我真的希望这成为可能,因为我有同样的问题。我最终将CurrentValueSubject
用于我的属性,而不是@Published
,因为它可以愉快地在协议中使用。
***.com/a/57657870/12106051
【参考方案1】:
我通过创建一个可以包含在协议中的通用 ObservableValue
类提出了一个相当干净的解决方法。
我不确定这是否有任何主要缺点,但它允许我轻松创建协议的模拟/可注入实现,同时仍然允许使用已发布的属性。
import Combine
class ObservableValue<T>
@Published var value: T
init(_ value: T)
self.value = value
protocol MyProtocol
var name: ObservableValue<String> get
var age: ObservableValue<Int> get
class MyImplementation: MyProtocol
var name: ObservableValue<String> = .init("bob")
var age: ObservableValue<Int> = .init(29)
class MyViewModel
let myThing: MyProtocol = MyImplementation()
func doSomething()
let myCancellable = myThing.age.$value
.receive(on: DispatchQueue.main)
.sink val in
print(val)
【讨论】:
【参考方案2】:我的 MVVM 方法:
// MARK: View
struct ContentView<ViewModel: ContentViewModel>: View
@ObservedObject var viewModel: ViewModel
var body: some View
VStack
Text(viewModel.name)
TextField("", text: $viewModel.name)
.border(Color.black)
struct ContentView_Previews: PreviewProvider
static var previews: some View
ContentView(viewModel: ContentViewModelMock())
// MARK: View model
protocol ContentViewModel: ObservableObject
var name: String get set
final class ContentViewModelImpl: ContentViewModel
@Published var name = ""
final class ContentViewModelMock: ContentViewModel
var name: String = "Test"
它是如何工作的:
ViewModel
协议继承了ObservableObject
,所以View
将订阅ViewModel
的变化
属性name
有getter 和setter,所以我们可以把它用作Binding
当View
更改name
属性(通过TextField)时,View 会通过ViewModel
中的@Published
属性通知更改(并且UI 会更新)
根据您的需要使用实际实现或模拟创建 View
可能的缺点:View
必须是通用的。
【讨论】:
几乎是我正在寻找的解决方案,非常感谢!完全有道理,Published 将强制整个 ObservableObject (viewModel) 触发刷新。【参考方案3】:我成功地只需要普通变量,并在实现类中添加@Published:
final class CustomListModel: IsSelectionListModel, ObservableObject
@Published var list: [IsSelectionListEntry]
init()
self.list = []
...
protocol IsSelectionListModel
var list: [IsSelectionListEntry] get
...
【讨论】:
【参考方案4】:试试这个
import Combine
import SwiftUI
// MARK: - View Model
final class MyViewModel: ObservableObject
@Published private(set) var value: Int = 0
func increment()
value += 1
extension MyViewModel: MyViewViewModel
// MARK: - View
protocol MyViewViewModel: ObservableObject
var value: Int get
func increment()
struct MyView<ViewModel: MyViewViewModel>: View
@ObservedObject var viewModel: ViewModel
var body: some View
VStack
Text("\(viewModel.value)")
Button("Increment")
self.viewModel.increment()
【讨论】:
虽然这段代码可能有助于解决问题,但它并没有解释为什么和/或如何回答问题。提供这种额外的背景将显着提高其长期价值。请edit您的答案添加解释,包括适用的限制和假设。【参考方案5】:我的同事想出的解决方法是使用声明属性包装器的基类,然后在协议中继承它。它仍然需要在符合协议的类中继承它,但看起来干净且运行良好。
class MyPublishedProperties
@Published var publishedProperty = "Hello"
protocol MyProtocol: MyPublishedProperties
func changePublishedPropertyValue(newValue: String)
class MyClass: MyPublishedProperties, MyProtocol
changePublishedPropertyValue(newValue: String)
publishedProperty = newValue
然后在执行中:
class MyViewModel
let myClass = MyClass()
myClass.$publishedProperty.sink string in
print(string)
myClass.changePublishedPropertyValue("World")
// prints:
// "Hello"
// "World"
【讨论】:
【参考方案6】:您必须明确并描述所有综合属性:
protocol WelcomeViewModel
var person: Person get
var personPublished: Published<Person> get
var personPublisher: Published<Person>.Publisher get
class ViewModel: ObservableObject
@Published var person: Person = Person()
var personPublished: Published<Person> _person
var personPublisher: Published<Person>.Publisher $person
【讨论】:
更新ViewModel
时你设置了哪个属性? . person
、. personPublished
或 . personPublisher
?【参考方案7】:
我们也遇到过这种情况。从 Catalina beta7 开始,似乎没有任何解决方法,因此我们的解决方案是通过如下扩展添加一致性:
struct IntView : View
@Binding var intValue: Int
var body: some View
Stepper("My Int!", value: $intValue)
protocol IntBindingContainer
var intValue$: Binding<Int> get
extension IntView : IntBindingContainer
var intValue$: Binding<Int> $intValue
虽然这有点额外的仪式,但我们可以向所有 IntBindingContainer
实现添加功能,如下所示:
extension IntBindingContainer
/// Reset the contained integer to zero
func resetToZero()
intValue$.wrappedValue = 0
【讨论】:
【参考方案8】:我认为应该这样做:
public protocol MyProtocol
var _person: Published<Person> get set
class MyClass: MyProtocol, ObservableObject
@Published var person: Person
public init(person: Published<Person>)
self._person = person
尽管编译器似乎有点喜欢它(至少是“类型”部分),但类和协议之间的属性访问控制不匹配(https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html)。我尝试了不同的组合:private
、public
、internal
、fileprivate
。但没有一个奏效。可能是一个错误?还是缺少功能?
【讨论】:
以上是关于如何定义协议以包含带有 @Published 属性包装器的属性的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI:“@Published”属性更改时未刷新“带有组的动态列表”
SwiftUI @Published 属性正在 DetailView 中更新
如何在双向绑定 ViewModel/TextField 中保持 @Published 属性标准化(即保持小写,删除链接等)?