【问题标题】:ObservedObject view-model is still in memory after the view is dismissed视图关闭后 ObservedObject 视图模型仍在内存中
【发布时间】:2020-06-19 16:41:54
【问题描述】:

我在 SwiftUI 和 Combine 中的内存管理方面遇到了一些问题。

例如,如果我有一个 NavigationView,然后导航到一个带有 TextField 的详细视图,然后在 TextField 中输入一个值并点击后退按钮,那么下次我转到该视图时,TextField 具有之前输入的值.

我注意到在关闭详细视图后视图模型仍在内存中,这可能就是 TextField 仍然保持值的原因。

在 UIKit 中,当关闭 ViewController 时,视图模型将被释放,然后在 ViewController 出现时再次创建。这似乎不是这里的情况。

我为这个问题附上了一些最低限度的可复制代码。

import SwiftUI
import Combine

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: OtherView()) {
                Text("Press Here")
            }
        }
    }
}

struct OtherView: View {

    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        VStack {
            TextField("Something", text: $viewModel.enteredText)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("Tap")
            }) {
                Text("Tapping")
            }.disabled(!viewModel.isValid)
        }
    }
}

class ViewModel: ObservableObject {

    @Published var enteredText = ""
    var isValid = false

    var cancellable: AnyCancellable?

    init() {
        cancellable = textValidatedPublisher.receive(on: RunLoop.main)
            .assign(to: \.isValid, on: self)
    }

    deinit {
        cancellable?.cancel()
    }

    var textValidatedPublisher: AnyPublisher<Bool, Never> {
        $enteredText.map {
            $0.count > 1
        }.eraseToAnyPublisher()
    }


}

我还注意到,例如,如果我在 OtherView 之后添加另一个视图,比如说 SomeOtherView,那么每次我从 OtherView 输入 TextField 时,就会调用 SomeOtherView 的视图模型中的 deinit。谁能解释一下为什么会这样?

【问题讨论】:

  • 发生这种情况是因为当您返回时ContentView 没有更改,因此它的body 永远不会重新评估。这意味着 OtherView 实例及其视图模型仅创建一次 - 第一次评估 ContentView 的主体。
  • 不是错误。这个NavigationLink(destination: OtherView()) 在body 调用上创建OtherView(),但不像人们想象的那样在导航时创建。因此,一旦您在 OtherView 构造时间创建视图模型,它的一个实例将始终以这种方法存在。 ...和上面的@NewDev +。
  • @Asperi 那么有没有办法在导航时实例化 OtherView?
  • 此外,我注意到如果我更改 ContetView 并重新评估视图,那么我将在内存中有两个 ViewModel

标签: ios swift swiftui combine


【解决方案1】:

此外,我注意到如果我更改 ContetView 并重新评估视图,那么我将在内存中拥有两个 ViewModel

这是由于ViewModel中的交叉引用,这里是固定变体

struct OtherView: View, Constructable {

    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        VStack {
            TextField("Something", text: $viewModel.enteredText)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("Tap")
            }) {
                Text("Tapping")
            }.disabled(!viewModel.isValid)
        }
        .onDisappear {
            self.viewModel.invalidate()     // << here !!
        }
    }
}

class ViewModel: ObservableObject {

    @Published var enteredText = ""
    var isValid = false

    var cancellable: AnyCancellable?

    init() {
        print("[>>] created")
        cancellable = textValidatedPublisher.receive(on: RunLoop.main)
            .assign(to: \.isValid, on: self)
    }

    func invalidate() {
        cancellable?.cancel()
        cancellable = nil
        print("[<<] invalidated")
    }

    deinit {
//        cancellable?.cancel()     // not here !!!
        print("[x] done")
    }

    var textValidatedPublisher: AnyPublisher<Bool, Never> {
        $enteredText.map {
            $0.count > 1
        }.eraseToAnyPublisher()
    }
}

--

更新:

导航时有没有办法实例化OtherView?

这是一个解决方案(使用 Xcode 11.4 / iOS 13.4 测试),但这只是成功的一半,因为一旦创建,它将一直存在,直到重新验证导航链接(即,在后面它会保留在内存中,直到下一次导航)

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: 
               // create wrapper view with type of view which creation
               // is deferred until navigation
               DeferCreatingView(of: OtherView.self)) {
                Text("Press Here")
            }
        }
    }
}

protocol Constructable {
    init()
}

struct DeferCreatingView<T: View & Constructable>: View {
    var ViewType: T.Type

    init(of type: T.Type) {
        ViewType = type
    }

    var body: some View {
        ViewType.init()     // << create only here
    }
}


struct OtherView: View, Constructable {
    // .. not changed code from first part
}

【讨论】:

    【解决方案2】:

    将导航视图样式.navigationViewStyle(StackNavigationViewStyle()) 添加到导航视图。它将取消初始化视图模型。请参阅下面的修改代码。

    import SwiftUI
    import Combine
    
    struct ContentView: View {
        var body: some View {
            NavigationView {
                NavigationLink(destination: OtherView()) {
                    Text("Press Here")
                }
            }
            .navigationViewStyle(StackNavigationViewStyle())
            // Added navigation style here.
        }
    }
    
    struct OtherView: View {
    
        @ObservedObject var viewModel = ViewModel()
    
        var body: some View {
            VStack {
                TextField("Something", text: $viewModel.enteredText)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
    
                Button(action: {
                    print("Tap")
                }) {
                    Text("Tapping")
                }.disabled(!viewModel.isValid)
            }
        }
    }
    
    class ViewModel: ObservableObject {
    
        @Published var enteredText = ""
        var isValid = false
    
        var cancellable: AnyCancellable?
    
        init() {
            cancellable = textValidatedPublisher.receive(on: RunLoop.main)
                .assign(to: \.isValid, on: self)
        }
    
        deinit {
            cancellable?.cancel()
        }
    
        var textValidatedPublisher: AnyPublisher<Bool, Never> {
            $enteredText.map {
                $0.count > 1
            }.eraseToAnyPublisher()
        }
    
    
    }
    

    【讨论】:

      猜你喜欢
      • 2014-11-02
      • 2011-09-18
      • 1970-01-01
      • 2020-03-24
      • 2018-08-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多