【问题标题】:How to define a protocol to include a property with @Published property wrapper如何定义协议以包含带有 @Published 属性包装器的属性
【发布时间】: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,因为它可以愉快地在协议中使用。

标签: protocols swiftui combine


【解决方案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)。我尝试了不同的组合:privatepublicinternalfileprivate。但没有一个奏效。可能是一个错误?还是缺少功能?

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2021-05-13
            • 2010-10-25
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-12-04
            • 2019-12-28
            • 2019-11-03
            相关资源
            最近更新 更多