17

当按照当前的 SwiftUI 语法使用@Published 属性包装器时,似乎很难定义一个包含@Published 属性的协议,或者我肯定需要帮助:)

当我在 View 和它的 ViewModel 之间实现依赖注入时,我需要定义一个 ViewModelProtocol 以便注入模拟数据以轻松预览。

这是我第一次尝试,

protocol PersonViewModelProtocol {
    @Published var person: Person
}

我得到“在协议中声明的属性‘人’不能有包装器”。

然后我尝试了这个,

protocol PersonViewModelProtocol {
    var $person: Published
}

显然没有用,因为 '$' 是保留的。

我希望有一种方法可以在 View 和它的 ViewModel 之间放置一个协议,并利用优雅的 @Published 语法。非常感谢。

4

8 回答 8

15

您必须明确并描述所有综合属性:

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 }
}
于 2019-10-20T09:22:14.367 回答
7

我的同事想出的解决方法是使用声明属性包装器的基类,然后在协议中继承它。它仍然需要在符合协议的类中继承它,但看起来很干净并且工作得很好。

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"
于 2019-12-19T19:07:46.990 回答
4

这就是我认为应该这样做的方式:

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. 但没有一个奏效。可能是一个错误?还是缺少功能?

于 2019-08-16T15:29:18.787 回答
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更改属性(通过 TextField)时,视图会通过属性中的name更改通知(并且 UI 会更新)@PublishedViewModel
  • View根据您的需要使用真实实现或模拟创建

可能的缺点:View必须是通用的。

于 2021-05-07T09:18:18.117 回答
0

我们也遇到过这种情况。从 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
    }
}

于 2019-09-04T16:15:34.243 回答
0

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

于 2022-01-01T00:52:46.100 回答
-1

尝试这个

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()
            }
        }
    }
}
于 2020-02-10T20:48:13.040 回答
-1

我成功地只需要普通变量,并在完成类中添加了@Published:

final class CustomListModel: IsSelectionListModel, ObservableObject {



    @Published var list: [IsSelectionListEntry]


    init() {

        self.list = []
    }
...
protocol IsSelectionListModel {


    var list: [IsSelectionListEntry] { get }
...
于 2020-03-30T12:24:10.700 回答