如何定义协议以包含带有 @Published 属性包装器的属性

问题描述 投票:0回答:10

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

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

这是我第一次尝试,

protocol PersonViewModelProtocol {
    @Published var person: Person
}

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

然后我尝试了这个,

protocol PersonViewModelProtocol {
    var $person: Published
}

显然不起作用,因为“$”被保留了。

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

protocols swiftui combine
10个回答
27
投票

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

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

20
投票

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

可能的缺点:

View
必须是通用的。


9
投票

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

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"

7
投票

我认为应该这样做:

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


1
投票

在 5.2 之前,我们不支持属性包装器。因此有必要手动公开发布者属性。

protocol PersonViewModelProtocol {
    var personPublisher: Published<Person>.Publisher { get }
}

class ConcretePersonViewModelProtocol: PersonViewModelProtocol {
    @Published private var person: Person

    // Exposing manually the person publisher 
    var personPublisher: Published<Person>.Publisher { $person }
    
    init(person: Person) {
        self.person = person
    }

    func changePersonName(name: String) {
        person.name = name
    }
}

final class PersonDetailViewController: UIViewController {
    
    private let viewModel = ConcretePersonViewModelProtocol(person: Person(name: "Joao da Silva", age: 60))

    private var cancellables: Set<AnyCancellable> = []

    func bind() {
        viewModel.personPublisher
            .receive(on: DispatchQueue.main)
            .sink { person in
                print(person.name)
            }
            .store(in: &cancellables)
        viewModel.changePersonName(name: "Joao dos Santos")
    }
}

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


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


0
投票

下面的程序怎么样?

  • 声明一个具有
    ViewState
    属性的
    @Published
  • 声明具有
    ViewModel
    属性要求的
    ViewState
    协议。
  • 创建一个
    ViewModel
    实例并将其
    ViewState
    实例提供给
    View
    ,它将观察到相同的情况。

ViewModel.swift

class ViewState {
    @Published fileprivate(set) var text: String = ""
    @Published fileprivate(set) var number: Int = 0
}

protocol ViewModel {
    var viewState: ViewState { get }
}

class ViewModelImpl: ViewModel {
    var viewState = ViewState()
    
    func onSomeChange() {
        viewState.text = "abc"
        viewState.number = 1
    }
}

查看.swift

class View: UIView {
    private var cancellables: [AnyCancellable] = []
    var viewState: ViewState? = nil {
        didSet {
            cancellables.removeAll()
            guard let viewState else { return }
            var textCancellable = viewState.$text.sink { recievedText in
                print("Do something")
            }
            var numberCancellable = viewState.$number.sink { recievedNumber in
                print("Do something")
            }
            cancellables = [textCancellable, numberCancellable]
        }
    }
}

-2
投票

试试这个

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

-2
投票

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

final class CustomListModel: IsSelectionListModel, ObservableObject {



    @Published var list: [IsSelectionListEntry]


    init() {

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


    var list: [IsSelectionListEntry] { get }
...
© www.soinside.com 2019 - 2024. All rights reserved.