Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you fix a SwiftUI memory leak caused by compound modifiers + animation?

Tags:

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.

enter image description here memory leak

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()
    }
}
like image 779
Jez Avatar asked Jan 27 '20 17:01

Jez


1 Answers

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.)

like image 145
Rob Napier Avatar answered Oct 02 '22 15:10

Rob Napier