当按照当前 SwiftUI 语法使用 @Published 属性包装器时,似乎很难定义一个包含 @Published 属性的协议,或者我肯定需要帮助:)
当我在 View 和 ViewModel 之间实现依赖注入时,我需要定义一个 ViewModelProtocol 以便注入模拟数据以轻松预览。
这是我第一次尝试,
protocol PersonViewModelProtocol {
@Published var person: Person
}
我得到“协议内声明的属性‘人’不能有包装器”。
然后我尝试了这个,
protocol PersonViewModelProtocol {
var $person: Published
}
显然不起作用,因为“$”被保留了。
我希望有一种方法可以在 View 和 ViewModel 之间建立协议,并利用优雅的 @Published 语法。非常感谢。
您必须明确并描述所有综合属性:
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 }
}
我的 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
必须是通用的。
我的同事提出的一个解决方法是使用声明属性包装器的基类,然后在协议中继承它。它仍然需要在您的类中继承它,该类也符合协议,但看起来干净并且工作得很好。
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"
我认为应该这样做:
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
。但没有一个奏效。可能是一个错误?或者缺少功能?
在 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")
}
}
我们也遇到过这种情况。从 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
}
}
我通过创建一个可以包含在协议中的通用
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)
}
}
}
下面的程序怎么样?
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]
}
}
}
试试这个
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()
}
}
}
}
我成功地只需要普通变量,并通过在实现类中添加@Published:
final class CustomListModel: IsSelectionListModel, ObservableObject {
@Published var list: [IsSelectionListEntry]
init() {
self.list = []
}
...
protocol IsSelectionListModel {
var list: [IsSelectionListEntry] { get }
...