如何将协议定义为@ObservedObject 属性的类型?

M.S*_*rag 23 protocols swiftui

我有一个依赖于视图模型的 swiftui 视图,视图模型有一些已发布的属性。我想为视图模型层次结构定义一个协议和默认实现,并使视图依赖于协议而不是具体类?

我希望能够写出以下内容:

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() } 
    }
}
Run Code Online (Sandbox Code Playgroud)

Asp*_*eri 42

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"))
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @NicolasDegen 我发现这也适用于`@EnvironmentObject`,只是它需要手动指定通用的具体类。例如 `NavigationLink(目的地: ItemView&lt;SomeItemModel&gt;()) { ... }` (2认同)

dws*_*erg 18

这篇文章与其他一些文章类似,但它只是发布变量所需的模板,不受干扰。

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

视图的通用约束让编译器知道它需要为使用该协议ViewModel: MyViewModel的任何类型构建逻辑MyViewModel


Pau*_*aul 10

我们通过编写自定义属性包装器在我们的小型库中找到了解决方案。你可以看看XUI

主要有两个问题:

  1. 中的相关类型要求ObservableObject
  2. 的一般约束ObservedObject

通过创建一个类似的协议ObservableObject(没有关联类型)和一个类似的协议包装器ObservedObject(没有通用约束),我们可以使这项工作成功!

让我先向您展示协议:

protocol AnyObservableObject: AnyObject {
    var objectWillChange: ObservableObjectPublisher { get }
}
Run Code Online (Sandbox Code Playgroud)

这本质上是 的默认形式ObservableObject,这使得新的和现有的组件很容易遵守该协议。

其次,属性包装器 - 它有点复杂,这就是为什么我将简单地添加一个链接。它有一个没有限制的通用属性,这意味着我们也可以将它与协议一起使用(目前只是语言限制)。但是,您需要确保仅将此类型与符合AnyObservableObject. 我们称之为属性包装器@Store

好的,现在让我们回顾一下创建和使用视图模型协议的过程:

  1. 创建视图模型协议
protocol ItemViewModel: AnyObservableObject {
    var title: String { get set }

    func save()
    func delete()
}
Run Code Online (Sandbox Code Playgroud)
  1. 创建视图模型实现
class MyItemViewModel: ItemViewModel, ObservableObject {

    @Published var title = ""

    func save() {}
    func delete() {}

}
Run Code Online (Sandbox Code Playgroud)
  1. 在您的视图中使用@Store属性包装器:
struct ListItemView: View {
    @Store var viewModel: ListItemViewModel

    var body: some View {
        // ...
    }

}
Run Code Online (Sandbox Code Playgroud)


小智 9

我认为类型擦除是最好的答案。

因此,您的协议保持不变。你有:

protocol ItemViewModel: ObservableObject {
    var title: String { get set }

    func save()
    func delete()
}
Run Code Online (Sandbox Code Playgroud)

因此,我们需要一个视图始终可以依赖的具体类型(如果太多视图在视图模型上变得通用,事情可能会变得疯狂)。所以我们将创建一个类型擦除实现。

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() }
}
Run Code Online (Sandbox Code Playgroud)

为了方便起见,我们还可以添加一个扩展来擦除,ItemViewModel并使用漂亮的尾随语法:

extension ItemViewModel {
   func eraseToAnyItemViewModel() -> AnyItemViewModel {
        AnyItemViewModel(wrapping: self)
   }
}
Run Code Online (Sandbox Code Playgroud)

此时你的观点可以是:

struct ItemView: View {
    @ObservedObject var viewModel: AnyItemViewModel

    var body: some View {
        TextField($viewModel.title, text: "Item Title")
        Button("Save") { self.viewModel.save() }  
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以像这样创建它(非常适合预览):

ItemView(viewModel: DummyItemViewModel().eraseToAnyItemViewModel())
Run Code Online (Sandbox Code Playgroud)

从技术上讲,您可以在视图初始值设定项中执行类型擦除,但实际上您必须编写该初始值设定项,而且这样做感觉有点不对劲。


Ami*_*n3t 8

好吧,我花了一些时间来弄清楚这些,但一旦我弄对了,一切就都有意义了。

目前无法在协议中使用 PropertyWrappers。但是您可以做的是在 View 中使用泛型,并期望 ViewModel 遵守您的协议。如果您正在测试某些东西或者需要为预览设置一些轻量级的东西,那么这特别有用。

我这里有一些示例,以便您可以正确使用

协议:

protocol UploadStoreProtocol:ObservableObject {

    var uploads:[UploadModel] {get set}

}
Run Code Online (Sandbox Code Playgroud)

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

对于 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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在您的视图已准备好获取 ViewModel,您可以像这样在外部设置您的商店:

@main
struct SampleApp: App {

    @StateObject var uploadStore = UploadStorage()

    var body: some Scene {
        WindowGroup {
            UploadView(store: uploadStore)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

对于预览版,您可以:

struct ContentView_Previews: PreviewProvider {

    @StateObject static var uploadStore = SamplePreviewStore()

    static var previews: some View {
        UploadView(store: uploadStore)
        
    }
}
Run Code Online (Sandbox Code Playgroud)