如何将协议定义为@ObservedObject 属性 的类型?
How to define a protocol as a type for a @ObservedObject property?
我有一个依赖视图模型的 swiftui 视图,该视图模型有一些已发布的属性。我想为视图模型层次结构定义一个协议和默认实现,并使视图依赖于协议而不是具体的 class?
我希望能够写出以下内容:
protocol ItemViewModel: ObservableObject {
@Published var title: String
func save()
func delete()
}
extension ItemViewModel {
@Published var title = "Some default Title"
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
struct ItemView: View {
@ObservedObject var viewModel: ItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
// What I have now is this:
class AbstractItemViewModel: ObservableObject {
@Published var title = "Some default Title"
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
class TestItemViewModel: AbstractItemViewModel {
func delete() {
// some custom behaviour
}
}
struct ItemView: View {
@ObservedObject var viewModel: AbstractItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
我不确定如何在协议中使用@属性 包装器。除此之外,正常的 swift 规则适用。
protocol ItemViewModel: ObservableObject {
var title: String{get set}
func save()
func delete()
}
extension ItemViewModel {
//var title = "Some default Title"
func save() {
// some default behaviour
title = "save in protocol"
print("save in protocol")
}
func delete() {
// some default behaviour
print("delete in protocol")
}
}
// What I have now is this:
class AbstractItemViewModel: ItemViewModel{
@Published var title = "Some default Title"
// func save() {
// print("save in class")
// // some default behaviour
// }
//
// func delete() {
// print("delete in class")
// // some default behaviour
// }
}
class TestItemViewModel: AbstractItemViewModel {
func delete() {
// some custom behaviour
title = "delete in"
print("delete in ")
}
}
struct ItemView: View {
@ObservedObject var viewModel: TestItemViewModel
var body: some View {
VStack{
Button(action: { self.viewModel.save()}){
Text("protocol save")
}
Button(action: { self.viewModel.delete()}){
Text("class delete")
}
TextField.init ("Item Title", text: $viewModel.title)}
}
}
swift 协议和扩展中不允许使用包装器和存储属性,至少现在是这样。所以我会采用以下混合协议、泛型和 类... 的方法(所有这些都可以编译并使用 Xcode 11.2 / iOS 13.2 进行测试)
// base model protocol
protocol ItemViewModel: ObservableObject {
var title: String { get set }
func save()
func delete()
}
// generic view based on protocol
struct ItemView<Model>: View where Model: ItemViewModel {
@ObservedObject var viewModel: Model
var body: some View {
VStack {
TextField("Item Title", text: $viewModel.title)
Button("Save") { self.viewModel.save() }
}
}
}
// extension with default implementations
extension ItemViewModel {
var title: String {
get { "Some default Title" }
set { }
}
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
// concrete implementor
class SomeItemModel: ItemViewModel {
@Published var title: String
init(_ title: String) {
self.title = title
}
}
// testing view
struct TestItemView: View {
var body: some View {
ItemView(viewModel: SomeItemModel("test"))
}
}
我认为类型擦除是对此的最佳答案。
因此,您的协议保持不变。你有:
protocol ItemViewModel: ObservableObject {
var title: String { get set }
func save()
func delete()
}
所以我们需要一个视图可以始终依赖的具体类型(如果太多视图在视图模型上变得通用,事情就会变得疯狂)。所以我们将创建一个类型擦除实现。
class AnyItemViewModel: ItemViewModel {
var title: title: String { titleGetter() }
private let titleGetter: () -> String
private let saver: () -> Void
private let deleter: () -> Void
let objectWillChange: AnyPublisher<Void, Never>
init<ViewModel: ItemViewModel>(wrapping viewModel: ViewModel) {
self.objectWillChange = viewModel
.objectWillChange
.map { _ in () }
.eraseToAnyPublisher()
self.titleGetter = { viewModel.title }
self.saver = viewModel.save
self.deleter = viewModel.delete
}
func save() { saver() }
func delete() { deleter() }
}
为方便起见,我们还可以添加一个扩展来擦除 ItemViewModel
,并使用漂亮的尾随语法:
extension ItemViewModel {
func eraseToAnyItemViewModel() -> AnyItemViewModel {
AnyItemViewModel(wrapping: self)
}
}
此时你的观点可以是:
struct ItemView: View {
@ObservedObject var viewModel: AnyItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
您可以这样创建它(非常适合预览):
ItemView(viewModel: DummyItemViewModel().eraseToAnyItemViewModel())
从技术上讲,您可以在视图初始化程序中执行类型擦除,但实际上您必须编写该初始化程序,这样做感觉有点不对劲。
我们通过编写自定义 属性 包装器在我们的小型库中找到了解决方案。你可以看看 XUI.
基本上有两个问题:
ObservableObject
中的关联类型要求
ObservedObject
的通用约束
通过创建一个类似于 ObservableObject
的协议(没有关联类型)和一个类似于 ObservedObject
的协议包装器(没有通用约束),我们可以做到这一点!
让我先给你看一下协议:
protocol AnyObservableObject: AnyObject {
var objectWillChange: ObservableObjectPublisher { get }
}
这实际上是 ObservableObject
的默认形式,这使得新组件和现有组件很容易遵守该协议。
其次,属性 包装器 - 它有点复杂,这就是为什么我将简单地添加一个 link。它有一个没有约束的通用属性,这意味着我们也可以将它与协议一起使用(目前只是语言限制)。但是,您需要确保仅将此类型与符合 AnyObservableObject
的对象一起使用。我们称之为 属性 包装器 @Store
.
好的,现在让我们来完成创建和使用视图模型协议的过程:
- 创建视图模型协议
protocol ItemViewModel: AnyObservableObject {
var title: String { get set }
func save()
func delete()
}
- 创建视图模型实现
class MyItemViewModel: ItemViewModel, ObservableObject {
@Published var title = ""
func save() {}
func delete() {}
}
- 在您的视图中使用
@Store
属性 包装器:
struct ListItemView: View {
@Store var viewModel: ListItemViewModel
var body: some View {
// ...
}
}
好吧,我花了一些时间来弄清楚这些,但是一旦我弄对了,一切就都有意义了。
目前无法在协议中使用 PropertyWrappers。但是您可以做的是在您的 View 中使用泛型,并期望您的 ViewModel 遵守您的协议。如果您正在测试东西或需要为预览设置一些轻量级的东西,这特别有用。
我这里有一些示例,因此您可以正确使用
协议:
protocol UploadStoreProtocol:ObservableObject {
var uploads:[UploadModel] {get set}
}
ViewModel:
您想确保您的视图模型是 ObservableObject
并将 @Published
添加到可以更改的变量
// For Preview
class SamplePreviewStore:UploadStoreProtocol {
@Published var uploads:[UploadModel] = []
init() {
uploads.append( UploadModel(id: "1", fileName: "Image 1", progress: 0, started: true, errorMessage: nil))
uploads.append( UploadModel(id: "2", fileName: "Image 2", progress: 47, started: true, errorMessage: nil))
uploads.append( UploadModel(id: "3", fileName: "Image 3", progress: 0, started: false, errorMessage: nil))
}
}
// Real Storage
class UploadStorage:UploadStoreProtocol {
@Published var uploads:[UploadModel] = []
init() {
uploads.append( UploadModel(id: "1", fileName: "Image 1", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "2", fileName: "Image 2", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "3", fileName: "Image 3", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "4", fileName: "Image 4", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "5", fileName: "Image 5", progress: 0, started: false, errorMessage: nil))
}
func addItem(){
uploads.append( UploadModel(id: "\(Int.random(in: 100 ... 100000))", fileName: "Image XX", progress: 0, started: false, errorMessage: nil))
}
func removeItemAt(index:Int){
uploads.remove(at: index)
}
}
对于 UI 视图,您可以使用泛型:
struct UploadView<ViewModel>: View where ViewModel:UploadStoreProtocol {
@ObservedObject var store:ViewModel
var body: some View {
List(store.uploads.indices){ item in
ImageRow(item: $store.uploads[item])
}.padding()
}
}
struct ImageRow: View {
@Binding var item:UploadModel
var body: some View {
HStack{
Image(item.id ?? "")
.resizable()
.frame(width: 50.0, height: 50.0)
VStack (alignment: .leading, spacing: nil, content: {
Text(item.fileName ?? "-")
Text(item.errorMessage ?? "")
.font(.caption)
.foregroundColor(.red)
})
Spacer()
VStack {
if (item.started){
Text("\(item.progress)").foregroundColor(.purple)
}
UploadButton(is_started: $item.started)
}
}
}
}
现在您的视图已准备好获取 ViewModel,您可以像这样在外部设置您的商店:
@main
struct SampleApp: App {
@StateObject var uploadStore = UploadStorage()
var body: some Scene {
WindowGroup {
UploadView(store: uploadStore)
}
}
}
对于预览,您可以拥有:
struct ContentView_Previews: PreviewProvider {
@StateObject static var uploadStore = SamplePreviewStore()
static var previews: some View {
UploadView(store: uploadStore)
}
}
这个 post 与其他一些类似,但它只是发布变量所需的模板,没有干扰。
protocol MyViewModel: ObservableObject {
var lastEntry: String { get }
}
class ActualViewModel: MyViewModel {
@Published private(set) var lastEntry: String = ""
}
struct MyView<ViewModel>: View where ViewModel: MyViewModel {
@ObservedObject var viewModel: ViewModel
var body: some View {
Text(viewModel.lastEntry)
}
}
View 的通用 ViewModel: MyViewModel
约束让编译器知道它需要为使用 MyViewModel
协议的任何类型构建逻辑
我有一个依赖视图模型的 swiftui 视图,该视图模型有一些已发布的属性。我想为视图模型层次结构定义一个协议和默认实现,并使视图依赖于协议而不是具体的 class?
我希望能够写出以下内容:
protocol ItemViewModel: ObservableObject {
@Published var title: String
func save()
func delete()
}
extension ItemViewModel {
@Published var title = "Some default Title"
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
struct ItemView: View {
@ObservedObject var viewModel: ItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
// What I have now is this:
class AbstractItemViewModel: ObservableObject {
@Published var title = "Some default Title"
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
class TestItemViewModel: AbstractItemViewModel {
func delete() {
// some custom behaviour
}
}
struct ItemView: View {
@ObservedObject var viewModel: AbstractItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
我不确定如何在协议中使用@属性 包装器。除此之外,正常的 swift 规则适用。
protocol ItemViewModel: ObservableObject {
var title: String{get set}
func save()
func delete()
}
extension ItemViewModel {
//var title = "Some default Title"
func save() {
// some default behaviour
title = "save in protocol"
print("save in protocol")
}
func delete() {
// some default behaviour
print("delete in protocol")
}
}
// What I have now is this:
class AbstractItemViewModel: ItemViewModel{
@Published var title = "Some default Title"
// func save() {
// print("save in class")
// // some default behaviour
// }
//
// func delete() {
// print("delete in class")
// // some default behaviour
// }
}
class TestItemViewModel: AbstractItemViewModel {
func delete() {
// some custom behaviour
title = "delete in"
print("delete in ")
}
}
struct ItemView: View {
@ObservedObject var viewModel: TestItemViewModel
var body: some View {
VStack{
Button(action: { self.viewModel.save()}){
Text("protocol save")
}
Button(action: { self.viewModel.delete()}){
Text("class delete")
}
TextField.init ("Item Title", text: $viewModel.title)}
}
}
swift 协议和扩展中不允许使用包装器和存储属性,至少现在是这样。所以我会采用以下混合协议、泛型和 类... 的方法(所有这些都可以编译并使用 Xcode 11.2 / iOS 13.2 进行测试)
// base model protocol
protocol ItemViewModel: ObservableObject {
var title: String { get set }
func save()
func delete()
}
// generic view based on protocol
struct ItemView<Model>: View where Model: ItemViewModel {
@ObservedObject var viewModel: Model
var body: some View {
VStack {
TextField("Item Title", text: $viewModel.title)
Button("Save") { self.viewModel.save() }
}
}
}
// extension with default implementations
extension ItemViewModel {
var title: String {
get { "Some default Title" }
set { }
}
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
// concrete implementor
class SomeItemModel: ItemViewModel {
@Published var title: String
init(_ title: String) {
self.title = title
}
}
// testing view
struct TestItemView: View {
var body: some View {
ItemView(viewModel: SomeItemModel("test"))
}
}
我认为类型擦除是对此的最佳答案。
因此,您的协议保持不变。你有:
protocol ItemViewModel: ObservableObject {
var title: String { get set }
func save()
func delete()
}
所以我们需要一个视图可以始终依赖的具体类型(如果太多视图在视图模型上变得通用,事情就会变得疯狂)。所以我们将创建一个类型擦除实现。
class AnyItemViewModel: ItemViewModel {
var title: title: String { titleGetter() }
private let titleGetter: () -> String
private let saver: () -> Void
private let deleter: () -> Void
let objectWillChange: AnyPublisher<Void, Never>
init<ViewModel: ItemViewModel>(wrapping viewModel: ViewModel) {
self.objectWillChange = viewModel
.objectWillChange
.map { _ in () }
.eraseToAnyPublisher()
self.titleGetter = { viewModel.title }
self.saver = viewModel.save
self.deleter = viewModel.delete
}
func save() { saver() }
func delete() { deleter() }
}
为方便起见,我们还可以添加一个扩展来擦除 ItemViewModel
,并使用漂亮的尾随语法:
extension ItemViewModel {
func eraseToAnyItemViewModel() -> AnyItemViewModel {
AnyItemViewModel(wrapping: self)
}
}
此时你的观点可以是:
struct ItemView: View {
@ObservedObject var viewModel: AnyItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
您可以这样创建它(非常适合预览):
ItemView(viewModel: DummyItemViewModel().eraseToAnyItemViewModel())
从技术上讲,您可以在视图初始化程序中执行类型擦除,但实际上您必须编写该初始化程序,这样做感觉有点不对劲。
我们通过编写自定义 属性 包装器在我们的小型库中找到了解决方案。你可以看看 XUI.
基本上有两个问题:
ObservableObject
中的关联类型要求
ObservedObject
的通用约束
通过创建一个类似于 ObservableObject
的协议(没有关联类型)和一个类似于 ObservedObject
的协议包装器(没有通用约束),我们可以做到这一点!
让我先给你看一下协议:
protocol AnyObservableObject: AnyObject {
var objectWillChange: ObservableObjectPublisher { get }
}
这实际上是 ObservableObject
的默认形式,这使得新组件和现有组件很容易遵守该协议。
其次,属性 包装器 - 它有点复杂,这就是为什么我将简单地添加一个 link。它有一个没有约束的通用属性,这意味着我们也可以将它与协议一起使用(目前只是语言限制)。但是,您需要确保仅将此类型与符合 AnyObservableObject
的对象一起使用。我们称之为 属性 包装器 @Store
.
好的,现在让我们来完成创建和使用视图模型协议的过程:
- 创建视图模型协议
protocol ItemViewModel: AnyObservableObject {
var title: String { get set }
func save()
func delete()
}
- 创建视图模型实现
class MyItemViewModel: ItemViewModel, ObservableObject {
@Published var title = ""
func save() {}
func delete() {}
}
- 在您的视图中使用
@Store
属性 包装器:
struct ListItemView: View {
@Store var viewModel: ListItemViewModel
var body: some View {
// ...
}
}
好吧,我花了一些时间来弄清楚这些,但是一旦我弄对了,一切就都有意义了。
目前无法在协议中使用 PropertyWrappers。但是您可以做的是在您的 View 中使用泛型,并期望您的 ViewModel 遵守您的协议。如果您正在测试东西或需要为预览设置一些轻量级的东西,这特别有用。
我这里有一些示例,因此您可以正确使用
协议:
protocol UploadStoreProtocol:ObservableObject {
var uploads:[UploadModel] {get set}
}
ViewModel:
您想确保您的视图模型是 ObservableObject
并将 @Published
添加到可以更改的变量
// For Preview
class SamplePreviewStore:UploadStoreProtocol {
@Published var uploads:[UploadModel] = []
init() {
uploads.append( UploadModel(id: "1", fileName: "Image 1", progress: 0, started: true, errorMessage: nil))
uploads.append( UploadModel(id: "2", fileName: "Image 2", progress: 47, started: true, errorMessage: nil))
uploads.append( UploadModel(id: "3", fileName: "Image 3", progress: 0, started: false, errorMessage: nil))
}
}
// Real Storage
class UploadStorage:UploadStoreProtocol {
@Published var uploads:[UploadModel] = []
init() {
uploads.append( UploadModel(id: "1", fileName: "Image 1", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "2", fileName: "Image 2", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "3", fileName: "Image 3", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "4", fileName: "Image 4", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "5", fileName: "Image 5", progress: 0, started: false, errorMessage: nil))
}
func addItem(){
uploads.append( UploadModel(id: "\(Int.random(in: 100 ... 100000))", fileName: "Image XX", progress: 0, started: false, errorMessage: nil))
}
func removeItemAt(index:Int){
uploads.remove(at: index)
}
}
对于 UI 视图,您可以使用泛型:
struct UploadView<ViewModel>: View where ViewModel:UploadStoreProtocol {
@ObservedObject var store:ViewModel
var body: some View {
List(store.uploads.indices){ item in
ImageRow(item: $store.uploads[item])
}.padding()
}
}
struct ImageRow: View {
@Binding var item:UploadModel
var body: some View {
HStack{
Image(item.id ?? "")
.resizable()
.frame(width: 50.0, height: 50.0)
VStack (alignment: .leading, spacing: nil, content: {
Text(item.fileName ?? "-")
Text(item.errorMessage ?? "")
.font(.caption)
.foregroundColor(.red)
})
Spacer()
VStack {
if (item.started){
Text("\(item.progress)").foregroundColor(.purple)
}
UploadButton(is_started: $item.started)
}
}
}
}
现在您的视图已准备好获取 ViewModel,您可以像这样在外部设置您的商店:
@main
struct SampleApp: App {
@StateObject var uploadStore = UploadStorage()
var body: some Scene {
WindowGroup {
UploadView(store: uploadStore)
}
}
}
对于预览,您可以拥有:
struct ContentView_Previews: PreviewProvider {
@StateObject static var uploadStore = SamplePreviewStore()
static var previews: some View {
UploadView(store: uploadStore)
}
}
这个 post 与其他一些类似,但它只是发布变量所需的模板,没有干扰。
protocol MyViewModel: ObservableObject {
var lastEntry: String { get }
}
class ActualViewModel: MyViewModel {
@Published private(set) var lastEntry: String = ""
}
struct MyView<ViewModel>: View where ViewModel: MyViewModel {
@ObservedObject var viewModel: ViewModel
var body: some View {
Text(viewModel.lastEntry)
}
}
View 的通用 ViewModel: MyViewModel
约束让编译器知道它需要为使用 MyViewModel
协议的任何类型构建逻辑