SwiftUI - 子类视图模型不会触发视图刷新

Posted

技术标签:

【中文标题】SwiftUI - 子类视图模型不会触发视图刷新【英文标题】:SwiftUI - Subclassed viewModel doesn't trigger view refresh 【发布时间】:2020-09-12 20:11:26 【问题描述】:

我有这种情况,我有一个包含一些常用元素的 BaseView 和一个包含一些常用功能的 BaseViewModel,但是当它的 @Published var 得到更新时,没有 BaseView 刷新发生。

设置是这样的:

class BaseViewModel: ObservableObject 

    @Published var overlayView: AnyView = EmptyView().convertToAnyView()

    func forceViewRefresh() 
        self.objectWillChange.send()
    

    func setOverlayView(overlayView: AnyView) 
        self.overlayView = overlayView
    


这个视图模型子类BaseViewModel

class FirstViewModel: BaseViewModel 
    func showOverlayView() 
        self.setOverlayView(overlayView: OverlayView().convertToAnyView())
    

我还有一个BaseView,我使用overlayView

struct BaseView<Content: View>: View 

let content: Content
@ObservedObject var viewModel = BaseViewModel()

init(content: () -> Content) 
    self.content = content()


var body: some View 
    
    ZStack 
        Color.green.edgesIgnoringSafeArea(.vertical)
        content
        viewModel.overlayView
    
    

显示的第一个视图是 FirstView,它符合 BaseViewProtocol 并具有扩展 BaseViewModelFirstViewModel

struct FirstView: BaseViewProtocol 

@ObservedObject var viewModel = FirstViewModel()

var body: some View 
    
    BaseView() 
        
        Button("Show overlay") 
            viewModel.showOverlayView()
        
     

单击First View 中的Show overlay 按钮会调用FirstViewModel 上的showOverlayView() 函数,后者又会调用BaseViewModel 上的setOverlayViewBaseViewModel 中的 overlayView 的值按预期更改,但不会调用 FirstView 上的视图刷新。 我做错了什么?

非常感谢。

【问题讨论】:

您想通过使用@Published var overlayView: AnyView = EmptyView().convertToAnyView() 来达到什么目的?这不是 SwiftUI 应该如何工作的......请分享更多代码,以便我们为您提供更多帮助。 我在 BaseViewModel 中设置了一个覆盖视图,以便它可以由其视图模型子类 BaseViewModel 的任何其他视图显示在布局中。当发布的 var 从默认的空视图更改为另一个视图时,它应该触发视图刷新,但不会。我没有看到任何与 SwiftUI “工作方式”背道而驰的东西。 (顺便说一句,convertToAnyView 只是我写的一个小扩展,它基本上等同于 AnyView(overlayView))。 但是,我已经编辑了原始帖子,添加了更多细节以更好地关注问题。 【参考方案1】:

我刚刚测试了这个代码示例,并且在 Xcode 12 beta 6 和 ios 14 beta 8 上运行良好

struct ContentView: View 
    
    @StateObject private var viewModel = FirstViewModel()
    
    var body: some View 
        ZStack 
            Button(action:  viewModel.showOverlayView() ) 
                Text("Press")
            
            viewModel.overlayView
        
    


class BaseViewModel: ObservableObject 
    
    @Published var overlayView: AnyView = AnyView(EmptyView())
    
    func forceViewRefresh() 
        self.objectWillChange.send()
    
    
    func setOverlayView(overlayView: AnyView) 
        self.overlayView = overlayView
    
    


class FirstViewModel: BaseViewModel 
    func showOverlayView() 
        self.setOverlayView(
            overlayView: AnyView(
                Color.blue
                    .opacity(0.2)
                    .allowsHitTesting(false)
            )
        )
    

【讨论】:

【参考方案2】:

通常在 SwiftUI 中,您不会在 body 之外创建视图。视图创建应该留给 SwiftUI - 相反,您可以定义一些其他控件来告诉 SwiftUI 如何何时 创建视图。

这是一个简化的演示,如何为不同的视图呈现不同的叠加层。

您可以创建一个基本的 OverlayView:

enum OverlayType 
    case overlay1, overlay2


struct OverlayView: View 
    let overlayType: OverlayType

    @ViewBuilder
    var body: some View 
        if overlayType == .overlay1 
            Text("Overlay1") // can be replaced with any view you want
        
        if overlayType == .overlay2 
            Text("Overlay1")
        
    

并在您的 BaseView 中使用它(如果 overlayTypenil,则不会显示叠加层):

struct BaseView<Content>: View where Content: View 
    let overlayType: OverlayType?
    let content: () -> Content

    var body: some View 
        ZStack 
            Color.green.edgesIgnoringSafeArea(.vertical)
            content()
            if overlayType != nil 
                OverlayView(overlayType: overlayType!)
            
        
    

现在您可以在 ContentView 中使用 BaseView 并指定其 OverlayType。

struct ContentView: View 
    @State var overlayType: OverlayType?

    var body: some View 
        BaseView(overlayType: overlayType) 
            Button("Show overlay") 
                overlayType = .overlay1
            
        
    


一些注意事项:

为简单起见,我使用@State 变量来控制覆盖。如果您的 ViewModel 有其他用例,您可能希望将逻辑移到那里。

请注意,最好使用@ViewBuilder,而不是AnyView

另外,如果你想在视图中观察ObservableObject,你需要使用@ObservedObject,而不是@ObservableObject

【讨论】:

谢谢,听起来不错,我试试。还要感谢您的最终建议,即使@ObservedObject 问题只是代表我的错字:-) 无论如何,我的代码中的主要问题是当发布的覆盖变量更新时 BaseView 不会刷新。有什么想法吗? @Goblin 只是不要在主体外部手动创建视图(在 SceneDelegate 中除外),您就不会遇到这些问题 - 正如我在回答的第一段中解释的那样。 对,但是覆盖是动态的,在运行时是未知的,所以事先创建它们是不可行的。即使我认识它们,如果我有 50 种不同的叠加层怎么办?无尽的枚举? @Goblin 这只是一个例子。但是,如果您有 50 个不同的叠加层,您目前的方法会发生什么? 50 个视图模型? 50个功能?如果叠加层是动态的并且在运行时未知,那么您为什么不在问题中提及这一点?

以上是关于SwiftUI - 子类视图模型不会触发视图刷新的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 视图不会为具有子类的已发布对象更新

SwiftUI:如果 @ObservedObject 是 UIViewController 的子类,则不会重新加载视图内容。这是一个错误还是我错过了啥?

从 @Binding var 修改 @State var 不会刷新 SwiftUI 中的视图

SwiftUI - 在列表中的视图内触发的动画也不会为列表设置动画

SwiftUI 和 MVVM - 模型和视图模型之间的通信

swiftui 视图不会更新,直到我最小化应用程序并再次打开它,即使我注入了模型