【问题标题】:Keep reference on view/data model after View update视图更新后保持对视图/数据模型的引用
【发布时间】:2020-02-03 02:02:19
【问题描述】:

假设我们有一个RootView 和一个DetailViewDetailView 有它自己的 BindableObject,我们称它为DetailViewModel 我们有场景:

  1. RootView 可能会被某种全球事件更新,例如错过 互联网连接或通过它自己的数据/视图模型
  2. RootView 处理事件时 内容已更新,这导致 DetailView 的新结构 被创建
  3. 如果DetailViewModel 是由DetailView 在初始化时创建的, 会有DetailViewModel 的另一个引用,它的状态(例如选定的对象)将被遗漏

我们怎样才能避免这种情况?

  1. 将所有 ViewModel 存储为 EnvironmentObjects,它基本上是一个单例池。这种方法会导致在不使用时将不需要的对象存储在内存中
  2. 将所有 ViewModel 从 RootView 传递给它的孩子和孩子的孩子(具有上述缺点 + 痛苦的依赖项)
  3. 将独立于视图的数据对象(又名工作人员)存储为环境对象。在那种情况下,我们在哪里存储对应于模型的视图依赖状态?如果我们将它存储在 View 中,最终会导致我们交叉更改 @States SwiftUI 禁止的内容
  4. 更好的方法?

对不起,我没有提供任何代码。这个问题是关于 Swift UI 的架构概念,我们尝试将声明性结构引用对象与数据结合起来。

目前我看不到保留与适当视图相对应的引用的方法,并且不要将它们永远以当前状态保留在内存/环境中。

更新:

让我们添加一些代码来看看如果 VM 是由它的 View 创建的会发生什么

import SwiftUI
import Combine

let trigger = Timer.publish(every: 2.0, on: .main, in: .default)

struct ContentView: View {

    @State var state: Date = Date()

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: ContentDetailView(), label: {
                    Text("Navigation push")
                        .padding()
                        .background(Color.orange)
                })
                Text("\(state)")
                    .padding()
                    .background(Color.green)
                ContentDetailView()
            }
        }
        .onAppear {
            _ = trigger.connect()
        }
        .onReceive(trigger) { (date) in
            self.state = date
        }
    }
}

struct ContentDetailView: View {

    @ObservedObject var viewModel = ContentDetailViewModel()
    @State var once = false

    var body: some View {
        let vmdesc = "View model uuid:\n\(viewModel.uuid)"
        print("State of once: \(once)")
        print(vmdesc)
        return Text(vmdesc)
            .multilineTextAlignment(.center)
            .padding()
            .background(Color.blue)
            .onAppear {
                self.once = true
            }
    }
}

class ContentDetailViewModel: ObservableObject, Identifiable {
    let uuid = UUID()
}

更新 2:

似乎如果我们将 ObservableObject 存储为视图中的@State(而不是 ObservedObject),视图会在 VM 上保持引用

@State var viewModel = ContentDetailViewModel()

有什么负面影响吗?我们可以这样使用吗?

更新 3:

看来如果 ViewModel 保持在 View 的 @State 中:

  1. 并且 ViewModel 通过具有强引用的闭包来保留 - deinit 永远不会被执行 -> 内存泄漏
  2. 并且 ViewModel 由具有弱引用的闭包保留 - deinit 每次在视图更新时调用,所有 subs 将被重置,但属性将相同

嗯……

更新 4:

这种方法还允许您在绑定闭包中保留强引用

import Foundation
import Combine
import SwiftUI

/**
 static func instanceInView() -> UIViewController {
     let vm = ContentViewModel()
     let vc = UIHostingController(rootView: ContentView(viewModel: vm))
     vm.bind(uiViewController: vc)
     return vc
 }
 */
public protocol ViewModelProtocol: class {
    static func instanceInView() -> UIViewController
    var bindings: Set<AnyCancellable> { get set }
    func onAppear()
    func onDisappear()
}

extension ViewModelProtocol {

    func bind(uiViewController: UIViewController) {
        uiViewController.publisher(for: \.parent)
            .sink(receiveValue: { [weak self] (parent) in
                if parent == nil {
                    self?.bindings.cancel()
                }
            })
            .store(in: &bindings)
    }

}

struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {

    func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
        return ViewModel.instanceInView()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
        //
    }
}
struct RootView: View {

    var body: some View {
        ModelView<ParkingViewModel>()
            .edgesIgnoringSafeArea(.vertical)
    }

}

【问题讨论】:

  • 我猜,您应该考虑 EnvironmentObject 方法,您的 RootView 将拥有 AppState 对象,并且他的所有孩子都可以访问此对象,所以 1) 你仍然可以通过一些全局事件来更新 AppState 2) RootView 在更新发生时重新创建 DetailView 3) DetailView 将创建 DetailViewModel@EnvironmentObject i> appState 对象。
  • 热衷于关注这里的发现。我遇到了与您遇到的相同问题,但对与架构无关的单一视图用例的最佳植入一无所知!
  • @ryannn 实际上我最终了解了它在 Flutter 中的工作原理。只需将其全部保存在 EnvironmentObject 或 EnvironmentValues 中,基本上它们都是能够在特定视图中被覆盖的单例,它们都代表应用程序状态。我建议你看一下关于 Flutter 应用程序设计的谷歌视频,比如youtube.com/watch?v=d_m5csmrf7I

标签: ios architecture swiftui


【解决方案1】:

Apple 说我们应该为 SwiftUI 之外的数据使用 ObservableObject。这意味着您必须自己管理数据源。

看起来单状态容器最适合 SwiftUI 架构。

typealias Reducer<State, Action> = (inout State, Action) -> Void

final class Store<State, Action>: ObservableObject {
 @Published private(set) var state: State

 private let reducer: Reducer<State, Action>

 init(initialState: State, reducer: @escaping Reducer<State, Action>) {
     self.state = initialState
     self.reducer = reducer
 }

 func send(_ action: Action) {
     reducer(&state, action)
 }
}

您可以将商店的实例传递到您的 SwiftUI 应用程序的环境中,它将在所有视图中可用,并将存储您的应用程序状态而不会丢失数据。

我写了一篇关于这种方法的博客文章,请查看它以获取更多信息 https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/

【讨论】:

  • SwfitUI 已经因为这些原因提供了 StateObject。但是,是的,这应该被认为是正确的答案。我最终将 Environment 用作具有 BLoC 模式的 Flutters Provider(在答案中似乎与 Redux 非常相似)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多