如何使用@Published属性包装器定义协议以包含属性

时间:2019-08-15 23:24:28

标签: protocols swiftui combine

使用遵循当前SwiftUI语法的@Published属性包装器时,很难定义一个包含带有@Published属性的协议,否则我肯定需要帮助:)

当我在View及其ViewModel之间实现依赖项注入时,我需要定义一个ViewModelProtocol以便注入模拟数据以方便预览。

这是我第一次尝试,

protocol PersonViewModelProtocol {
    @Published var person: Person
}

我收到“协议中声明的属性“人”不能具有包装器”。

然后我尝试了这个

protocol PersonViewModelProtocol {
    var $person: Published
}

由于保留了“ $”,显然无法正常工作。

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

7 个答案:

答案 0 :(得分:3)

这是我应该做的事情:

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

答案 1 :(得分: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
    }
}

答案 2 :(得分:0)

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

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

答案 3 :(得分:0)

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

# make first pie without labels:
bigger = plt.pie(first_sizes, colors=flatui,
                 startangle=90, frame=True, radius = 1,
                wedgeprops={'edgecolor':'k'})

# feed labels to legend:
plt.gca().legend(first_labels, loc='center right', bbox_to_anchor=(1,0,0.5,0.5))

然后实施:

getCollections()

答案 4 :(得分:0)

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

Test suite failed to run

Jest encountered an unexpected token

This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.

By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".

Here's what you can do:
 • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
 • If you need a custom transformation specify a "transform" option in your config.
 • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/en/configuration.html

Details:

/*path-to-project*/packages/app/src/lib/breadcrumbs/__tests__/utils/getBreadcrumbContext.test.js:37
var _ref = import('../../__mocks__'),
           ^^^^^^

SyntaxError: Unexpected token import

  at ScriptTransformer._transformAndBuildScript (../../node_modules/jest-runtime/build/script_transformer.js:403:17)
final class CustomListModel: IsSelectionListModel, ObservableObject {



    @Published var list: [IsSelectionListEntry]


    init() {

        self.list = []
    }
...

答案 5 :(得分:0)

我的 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)时,会通过 @Published 中的 ViewModel 属性通知 View(并且 UI 已更新)
  • 根据您的需要使用实际实现或模拟创建 View

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

答案 6 :(得分:-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()
            }
        }
    }
}