关闭视图后,ObservedObject视图模型仍在内存中

时间:2020-06-19 16:41:54

标签: ios swift swiftui combine

我在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。谁能解释一下为什么会这样吗?

2 个答案:

答案 0 :(得分:3)

此外,我注意到,如果我对ContetView进行了更改并且对视图进行了重新评估,那么内存中将有两个ViewModels

这是由于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
}

答案 1 :(得分:0)

将导航视图样式 @font-face { font-family: "Gilroy"; src: local("Gilroy"), url(../fonts/Gilroy-Regular.otf) format("opentype"), // font-weight: 400 url(../fonts/Gilroy-SemiBold.otf) format("opentype"), // font-weight: 600 url(../fonts/Gilroy-Bold.otf) format("opentype"); // font-weight: 700 } body { overflow-x: hidden; font-family: 'Gilroy', sans-serif; font-display: swap; } h1, h2, h3, h4, h5, h6, p { color: $text-color; } 添加到导航视图。它将取消初始化视图模型。请参阅下面的修改后的代码。

.navigationViewStyle(StackNavigationViewStyle())