I'm displaying file structure using DisclosureGroup
.
And I encountered a memory leak problem.
Code
Node
Node
represents a file or a folder.
isExpanded
is used to indicate if the child nodes are visible.
If true
, it will find its child nodes, which are set into children
.
If false
, it will clear children
for releasing references.
class Node: ObservableObject, Identifiable, Hashable, CustomStringConvertible {
// ...
@Published var name: String
@Published var children: [Node]?
@Published var isExpanded = false {
willSet {
if self.isFile {
// This node represents a file.
// It does not have any children.
return
}
if newValue {
if children?.count == 0 {
DispatchQueue.main.async {
// get child nodes
self.children = childrenOf(self.url)
}
}
} else {
if children?.count != 0 {
DispatchQueue.main.async {
// collapse child nodes
self.children?.forEach { child in
child.isExpanded = false
}
// clear children when this node is collapsed
self.children = []
}
}
}
}
}
init(/*...*/) {
// ...
print("init \(name)")
}
deinit {
// ...
print("deinit \(name)")
}
// ...
}
For convenience, I print some messages when initializing Node
and deinitializing Node
.
TreeNode
TreeNode
displays Node
using DisclosureGroup
.
struct TreeNode: View {
@ObservedObject var parent: Node
@ObservedObject var node: Node
var body: some View {
if node.isFile {
Text(node.name)
} else {
DisclosureGroup(
isExpanded: $node.isExpanded,
content: {
if node.isExpanded {
ForEach(node.children ?? []) { child in
TreeNode(parent: node, node: child)
}
}
},
label: {
FolderNodeView(node: node)
}
)
}
}
}
struct FolderNodeView: View {
@ObservedObject var node: Node
var body: some View {
Label(
title: { Text(node.name) },
icon: { Image(systemName: "folder.fill") }
)
}
}
I use if node.isExpanded
for lazy loading.
When node.isExpanded
is true
, it will show node's children and print initialization messages. Otherwise, it will hide child nodes and print deinitialization messages.
But unexpectedly it does not print any deinitialization messages when the node is collapsed. This indicates that it retains references and therefore these Node
objects still exists in memory causing memory leak.
Demo
When the node is expanded, its child nodes will be displayed after loading is completed. The code works correctly.
Then I collapsed the node, it didn't print any deinitialization messages. And when I expanded it again, it initialized new nodes and deinitialized the old nodes at this time. Deinitialization seems to be delayed.
So I guess TreeNode
retains references when content is hidden.
Then I deleted TreeNode
in ForEach
.
DisclosureGroup(
isExpanded: $node.isExpanded,
content: {
if node.isExpanded {
ForEach(node.children ?? []) { child in
// TreeNode(parent: node, node: child)
}
}
},
label: {
FolderNodeView(node: node)
}
)
It cannot display the child nodes. But it released reference correctly. So the code works expectedly.
After that, I tried to replace TreeNode
with Text
or Label
.
I found that none of them released references immediately when I collapsed the node.
Why did this happen?
Any idea how to fix it?