【问题标题】:SwiftUI - Subclassed viewModel doesn't trigger view refreshSwiftUI - 子类视图模型不会触发视图刷新
【发布时间】: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))。
  • 但是,我已经编辑了原始帖子,添加了更多细节以更好地关注问题。

标签: swiftui


【解决方案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个功能?如果叠加层是动态的并且在运行时未知,那么您为什么不在问题中提及这一点?
    猜你喜欢
    • 1970-01-01
    • 2021-06-01
    • 1970-01-01
    • 2019-11-29
    • 1970-01-01
    • 2020-04-05
    • 1970-01-01
    • 1970-01-01
    • 2022-01-05
    相关资源
    最近更新 更多