视图关闭后,ObservedObject 视图模型仍在内存中
ObservedObject view-model is still in memory after the view is dismissed
我在 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 {
[=10=].count > 1
}.eraseToAnyPublisher()
}
}
我还注意到,例如,如果我添加另一个视图,比如说在 OtherView 之后添加 SomeOtherView,那么每次我从 OtherView 中键入 TextField 时,都会调用 SomeOtherView 的视图模型中的 deinit。任何人都可以解释为什么会这样吗?
Moreover, I noticed that if I to a change in ContetView and the view is reevaluated, then I will have two ViewModels in memory
由于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 {
[=10=].count > 1
}.eraseToAnyPublisher()
}
}
--
更新:
is there a way to instantiate OtherView when navigating?
这是一个解决方案(使用 Xcode 11.4 / iOS 13.4 测试),但这只是事半功倍,因为一旦创建它就会一直存在直到导航 link 重新验证(即在后面它保留在内存中直到下一次导航)
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
}
将导航视图样式 .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 {
[=10=].count > 1
}.eraseToAnyPublisher()
}
}
我在 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 {
[=10=].count > 1
}.eraseToAnyPublisher()
}
}
我还注意到,例如,如果我添加另一个视图,比如说在 OtherView 之后添加 SomeOtherView,那么每次我从 OtherView 中键入 TextField 时,都会调用 SomeOtherView 的视图模型中的 deinit。任何人都可以解释为什么会这样吗?
Moreover, I noticed that if I to a change in ContetView and the view is reevaluated, then I will have two ViewModels in memory
由于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 {
[=10=].count > 1
}.eraseToAnyPublisher()
}
}
--
更新:
is there a way to instantiate OtherView when navigating?
这是一个解决方案(使用 Xcode 11.4 / iOS 13.4 测试),但这只是事半功倍,因为一旦创建它就会一直存在直到导航 link 重新验证(即在后面它保留在内存中直到下一次导航)
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
}
将导航视图样式 .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 {
[=10=].count > 1
}.eraseToAnyPublisher()
}
}