【问题标题】:SwiftUI View don't see property of ObservableObject marked with @PublishedSwiftUI 视图看不到标记为 @Published 的 ObservableObject 的属性
【发布时间】:2021-12-18 14:27:15
【问题描述】:

我正在使用 SwiftUI 和 VIPER 编写我的应用程序。为了节省 viper(可测试性、协议等)和 SwiftUI 反应性的想法,我想再添加 1 层 - ViewModel。我的演示者将向交互器询问数据并将其放入 ViewModel,然后视图将读取此值。我检查了将数据放入视图模型的方法是否有效 - 是的。但是我的视图只是看不到视图模型的属性(显示空列表),即使它符合 ObservableObject 并且属性标记为已发布。更有趣的是,如果我将数据存储在 Presenter 中并用已发布和可观察的对象对其进行标记,它将起作用。提前致谢!

class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
    var interactor: BeersListInteractorProtocol
    @ObservedObject var viewModel = BeersListViewModel()
    
    init(interactor: BeersListInteractorProtocol){
        self.interactor = interactor
        
    }
    func loadList(at page: Int){
        interactor.loadList(at: page) { beers in
            DispatchQueue.main.async {
                self.viewModel.beers.append(contentsOf: beers)
                print(self.viewModel.beers)
            }
        }
    }


class BeersListViewModel:ObservableObject{
    @Published var beers = [Beer]()
}


struct BeersListView: View{
    var presenter : BeersListPresenterProtocol
    @StateObject var viewModel : BeersListViewModel
    var body: some View {
        NavigationView{
            List{
                ForEach(viewModel.beers, id: \.id){ beer in
                    HStack{
                        VStack(alignment: .leading){
                            Text(beer.name)
                                .font(.headline)
                            Text("Vol: \(presenter.formattedABV(beer.abv))")
                                .font(.subheadline)
                            
                        }

【问题讨论】:

  • @ObservedObject 应该在视图中。
  • 基本上你违反了两条规则。 @StateObject 归封闭视图所有,必须初始化。另一方面,@ObservedObect 由更高级别的对象拥有,并且引用通过初始化程序传递。
  • 您需要订阅BeersListViewModelBeersListPresenter 中的发布值。 @ObservedObject 仅适用于视图结构。

标签: swiftui observableobject viper-architecture


【解决方案1】:

一些注意事项。

您不能链接ObservableObjects,所以@ObservedObject var viewModel = BeersListViewModel() 内的class 将不起作用。

第二个你有 2 个 ViewModel 一个在 View 和一个在 Presenter 你必须选择一个。一个人不会知道另一个人在做什么。

下面是如何让你的代码工作

import SwiftUI
struct Beer: Identifiable{
    var id: UUID = UUID()
    var name: String = "Hops"
    var abv: String = "H"
}
protocol BeersListInteractorProtocol{
    func loadList(at: Int, completion: ([Beer])->Void)
}

struct BeersListInteractor: BeersListInteractorProtocol{
    func loadList(at: Int, completion: ([Beer]) -> Void) {
        completion([Beer(), Beer(), Beer()])
    }
}
protocol BeersListPresenterProtocol: ObservableObject{
    var interactor: BeersListInteractorProtocol { get set }
    var viewModel : BeersListViewModel { get set }
    
    func formattedABV(_ abv: String) -> String
    func loadList(at page: Int)
}
class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
    var interactor: BeersListInteractorProtocol
    //You can't chain `ObservedObject`s
    @Published var viewModel = BeersListViewModel()
    
    init(interactor: BeersListInteractorProtocol){
        self.interactor = interactor
        
    }
    func loadList(at page: Int){
        interactor.loadList(at: page) { beers in
            DispatchQueue.main.async {
                self.viewModel.beers.append(contentsOf: beers)
                print(self.viewModel.beers)
            }
        }
    }
    func formattedABV(_ abv: String) -> String{
        "**\(abv)**"
    }
}

//Change to struct
struct BeersListViewModel{
    var beers = [Beer]()
}


struct BeerListView<T: BeersListPresenterProtocol>: View{
    //This is what will trigger view updates
    @StateObject var presenter : T
    //The viewModel is in the Presenter
    //@StateObject var viewModel : BeersListViewModel
    var body: some View {
        NavigationView{
            List{
                Button("load list", action: {
                    presenter.loadList(at: 1)
                })
                ForEach(presenter.viewModel.beers, id: \.id){ beer in
                    HStack{
                        VStack(alignment: .leading){
                            Text(beer.name)
                                .font(.headline)
                            Text("Vol: \(presenter.formattedABV(beer.abv))")
                                .font(.subheadline)
                            
                        }
                    }
                }
            }
        }
    }
}
struct BeerListView_Previews: PreviewProvider {
    static var previews: some View {
        BeerListView(presenter: BeersListPresenter(interactor: BeersListInteractor()))
    }
}

现在我无论如何都不是 VIPER 专家,但我认为你在混合概念。混合使用 MVVM 和 VIPER。因为在 VIPER 中,presenter 存在于 View/ViewModel 之下,而不是处于同一级别。

我不久前找到了this 教程。它适用于 UIKit,但如果我们使用 ObservableObject 代替 UIViewController 而 SwiftUI View 充当故事板。

它使ViewModel(即ObservableObject)和View(即SwiftUI struct)在VIPER 方面成为单个View 层。

你会得到类似这样的代码

protocol BeersListPresenterProtocol{
    var interactor: BeersListInteractorProtocol { get set }
    
    func formattedABV(_ abv: String) -> String
    func loadList(at: Int, completion: ([Beer]) -> Void)
    
}
struct BeersListPresenter: BeersListPresenterProtocol{
    var interactor: BeersListInteractorProtocol
    
    init(interactor: BeersListInteractorProtocol){
        self.interactor = interactor
        
    }
    func loadList(at: Int, completion: ([Beer]) -> Void) {
        
        interactor.loadList(at: at) { beers in
            completion(beers)
        }
    }
    func formattedABV(_ abv: String) -> String{
        "**\(abv)**"
    }
}
protocol BeersListViewProtocol: ObservableObject{
    var presenter: BeersListPresenterProtocol { get set }
    var beers: [Beer] { get set }
    func loadList(at: Int)
    func formattedABV(_ abv: String) -> String
    
}
class BeersListViewModel: BeersListViewProtocol{
    @Published var presenter: BeersListPresenterProtocol
    @Published var beers: [Beer] = []
    
    init(presenter: BeersListPresenterProtocol){
        self.presenter = presenter
    }
    func loadList(at: Int) {
        DispatchQueue.main.async {
            self.presenter.loadList(at: at, completion: {beers in
                self.beers = beers
            })
        }
    }
    
    func formattedABV(_ abv: String) -> String {
        presenter.formattedABV(abv)
    }
}
struct BeerListView<T: BeersListViewProtocol>: View{
    @StateObject var viewModel : T
    
    var body: some View {
        NavigationView{
            List{
                Button("load list", action: {
                    viewModel.loadList(at: 1)
                })
                ForEach(viewModel.beers, id: \.id){ beer in
                    HStack{
                        VStack(alignment: .leading){
                            Text(beer.name)
                                .font(.headline)
                            Text("Vol: \(viewModel.formattedABV(beer.abv))")
                                .font(.subheadline)
                            
                        }
                    }
                }
            }
        }
    }
}
struct BeerListView_Previews: PreviewProvider {
    static var previews: some View {
        BeerListView(viewModel: BeersListViewModel(presenter: BeersListPresenter(interactor: BeersListInteractor())))
    }
}

如果您不想将 VIPER 视图层分为 ViewModelSwiftUI View 您可以选择执行以下代码之类的操作,但这会使替换 UI 变得更加困难,并且通常是不是一个好习惯。因为当interactor 有更新时,您将无法从presenter 调用方法。

struct BeerListView<T: BeersListPresenterProtocol>: View, BeersListViewProtocol{
    var presenter: BeersListPresenterProtocol
    @State var beers: [Beer] = []
    
    var body: some View {
        NavigationView{
            List{
                Button("load list", action: {
                    loadList(at: 1)
                })
                ForEach(beers, id: \.id){ beer in
                    HStack{
                        VStack(alignment: .leading){
                            Text(beer.name)
                                .font(.headline)
                            Text("Vol: \(formattedABV(beer.abv))")
                                .font(.subheadline)
                            
                        }
                    }
                }
            }
        }
    }
    func loadList(at: Int) {
        DispatchQueue.main.async {
            self.presenter.loadList(at: at, completion: {beers in
                self.beers = beers
            })
        }
    }
    
    func formattedABV(_ abv: String) -> String {
        presenter.formattedABV(abv)
    }
}
struct BeerListView_Previews: PreviewProvider {
    static var previews: some View {
        BeerListView<BeersListPresenter>(presenter: BeersListPresenter(interactor: BeersListInteractor()))
    }
}

【讨论】:

    猜你喜欢
    • 2021-10-14
    • 1970-01-01
    • 2021-01-31
    • 2021-01-25
    • 2020-10-21
    • 2019-12-28
    • 2021-03-23
    • 2020-07-31
    • 1970-01-01
    相关资源
    最近更新 更多