How can I fix a memory leak which happens when I use compound modifiers with an animation?
In this example we have 4 MySquareView
squares that have an animated rotation effect, and these sit within a ZStack in ContentView
which has a scale modifier. As you can see the memory in use continues to increase over time.
The same issue seems to happen with other modifiers too. Full example:
import SwiftUI
struct MySquare: Identifiable, Hashable {
var id = UUID()
var offsetX: CGFloat
var offsetY: CGFloat
}
struct MySquareView: View {
@State private var rotateSquare = true
var body: some View {
Rectangle()
.fill(Color.purple)
.frame(width: 30, height: 30)
.rotationEffect(.degrees(self.rotateSquare ? -25 : 25))
.onAppear(perform: {
withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
self.rotateSquare.toggle()
}
})
}
}
struct ContentView: View {
var mySquares = [
MySquare(offsetX: -40, offsetY: 40),
MySquare(offsetX: 50, offsetY: -20),
MySquare(offsetX: -10, offsetY: 80),
MySquare(offsetX: 110, offsetY: 20)
]
var body: some View {
VStack {
ZStack {
ForEach(mySquares, id: \.self) { mySquare in
MySquareView()
.offset(x: mySquare.offsetX, y: mySquare.offsetY)
}
}
.scaleEffect(0.8)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This appears to be related to embedding mutation inside a ForEach, which looking at the memory graphs may involve List (which also seems to have a memory leak when its elements are mutated). I recommend opening a Feedback on this.
It can be eliminated by removing the ForEach:
func square(at offset: Int) -> some View {
let mySquare = mySquares[offset]
return MySquareView()
.offset(x: mySquare.offsetX,
y: mySquare.offsetY)
}
var body: some View {
VStack {
ZStack {
square(at: 0)
square(at: 1)
square(at: 2)
square(at: 3)
}
.scaleEffect(0.8)
}
}
You can also hack your way around this by injecting a recursive AnyView rather than using ForEach. There may be other clever solutions like this; it may be worth exploring further, since losing both ForEach and List is quite obnoxious.
func loopOver<C: Collection, V: View>(_ list: C, content: (C.Element) -> V) -> AnyView
{
guard let element = list.first else { return AnyView(EmptyView()) }
return AnyView(Group {
content(element)
loopOver(list.dropFirst(), content: content)
})
}
var body: some View {
VStack {
ZStack {
loopOver(mySquares) { MySquareView().offset(x: $0.offsetX, y: $0.offsetY )}
}
.scaleEffect(0.8)
}
}
(Using AnyView like this will get in the way of various SwiftUI optimizations, so it's a last resort, but in this case it is likely necessary.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With