【问题标题】:SwiftUI - memory leak in NavigationViewSwiftUI - NavigationView 中的内存泄漏
【发布时间】:2020-08-01 20:01:24
【问题描述】:
我正在尝试向模态显示的视图导航栏添加一个关闭按钮。但是,解雇后,我的视图模型 deinit 方法永远不会被调用。我发现问题在于它在 navigationBarItem 中捕获了 self。我不能只在 navigationBarItem 的操作中传递weak self,因为 View 是一个结构,而不是一个类。这是一个有效的问题还是只是缺乏知识?
struct ModalView: View {
@Environment(\.presentationMode) private var presentation: Binding<PresentationMode>
@ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
Text("Modal is presented")
.navigationBarItems(leading:
Button(action: {
// works after commenting this line
self.presentation.wrappedValue.dismiss()
}) {
Text("close")
}
)
}
}
}
【问题讨论】:
标签:
ios
swift
memory-leaks
swiftui
combine
【解决方案1】:
由于navigationBarItems,我遇到了严重的内存泄漏,并将我的视图模型传递给我用作条形项目的视图。
四处挖掘,我了解到navigationBarItems is deprecated
我有
.navigationBarItems(trailing:
AlbumItemsScreenNavButtons(viewModel: viewModel)
)
替换为toolbar。
我现在的用法是这样的:
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
AlbumItemsScreenNavButtons(viewModel: viewModel)
}
}
【解决方案2】:
我的解决办法是
.navigationBarItems(
trailing: self.filterButton
)
..........................................
var filterButton: some View {
Button(action: {[weak viewModel] in
viewModel?.showFilter()
},label: {
Image("search-filter-icon").renderingMode(.original)
})
}
【解决方案3】:
您不需要在自己的视图中拆分关闭按钮。您可以通过在 NavigationView 的闭包中添加 capture list 来解决此内存泄漏问题:这将破坏保留您的 viewModel 的引用循环。
您可以将此示例代码复制/粘贴到 Playground 中,看看它是否解决了问题(Xcode 11.4.1,iOS Playground)。
import SwiftUI
import PlaygroundSupport
struct ModalView: View {
@Environment(\.presentationMode) private var presentation
@ObservedObject var viewModel: ViewModel
var body: some View {
// Capturing only the `presentation` property to avoid retaining `self`, since `self` would also retain `viewModel`.
// Without this capture list (`->` means `retains`):
// self -> body -> NavigationView -> Button -> action -> self
// this is a retain cycle, and since `self` also retains `viewModel`, it's never deallocated.
NavigationView { [presentation] in
Text("Modal is presented")
.navigationBarItems(leading: Button(
action: {
// Using `presentation` without `self`
presentation.wrappedValue.dismiss()
},
label: { Text("close") }))
}
}
}
class ViewModel: ObservableObject { // << tested view model
init() {
print(">> inited")
}
deinit {
print("[x] destroyed")
}
}
struct TestNavigationMemoryLeak: View {
@State private var showModal = false
var body: some View {
Button("Show") { self.showModal.toggle() }
.sheet(isPresented: $showModal) { ModalView(viewModel: ViewModel()) }
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(TestNavigationMemoryLeak())
【解决方案4】:
我推荐设计级解决方案,即。将导航栏项目分解为单独的视图组件会破坏导致泄漏的不希望的循环引用。
使用 Xcode 11.4 / iOS 13.4 测试 - ViewModel 按预期销毁。
这里是完整的测试模块代码:
struct CloseBarItem: View { // separated bar item with passed binding
@Binding var presentation: PresentationMode
var body: some View {
Button(action: {
self.presentation.dismiss()
}) {
Text("close")
}
}
}
struct ModalView: View {
@Environment(\.presentationMode) private var presentation
@ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
Text("Modal is presented")
.navigationBarItems(leading:
CloseBarItem(presentation: presentation)) // << decompose
}
}
}
class ViewModel: ObservableObject { // << tested view model
init() {
print(">> inited")
}
deinit {
print("[x] destroyed")
}
}
struct TestNavigationMemoryLeak: View {
@State private var showModal = false
var body: some View {
Button("Show") { self.showModal.toggle() }
.sheet(isPresented: $showModal) { ModalView(viewModel: ViewModel()) }
}
}
struct TestNavigationMemoryLeak_Previews: PreviewProvider {
static var previews: some View {
TestNavigationMemoryLeak()
}
}