如何定义协议以包含带有 @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,所以我们可以把它用作BindingView 更改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)。我尝试了不同的组合:privatepublicinternalfileprivate。但没有一个奏效。可能是一个错误?还是缺少功能?

【讨论】:

以上是关于如何定义协议以包含带有 @Published 属性包装器的属性的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI:“@Published”属性更改时未刷新“带有组的动态列表”

SwiftUI @Published 属性正在 DetailView 中更新

如何在双向绑定 ViewModel/TextField 中保持 @Published 属性标准化(即保持小写,删除链接等)?

当从另一个 ObservedObject 链接到 Published 属性时,视图不会对更改做出反应

@Published 用于计算属性(或最佳解决方法)

如何“转发”@Published 值