Search code examples
swiftswiftuimemory-leaksswiftui-animation

Memory leak in iOS 17 in AnimatableModifier


I'm facing a memory leak problem in iOS 17, in earlier versions it works fine:

import SwiftUI

struct SkeletonModifier: ViewModifier {

    @State private var phase: CGFloat = 0
    let animation: Animation

    init(animation: Animation) {
        self.animation = animation
    }

    func body(content: Content) -> some View {
        content
            .modifier(
                AnimatedMask(phase: phase).animation(animation)
            )
           .onAppear { phase = 0.8 }
     }

     struct AnimatedMask: AnimatableModifier {

         var phase: CGFloat = 0

        var animatableData: CGFloat {
            get { phase }
            set { phase = newValue }
        }

        func body(content: Content) -> some View {
             content
                 .mask(GradientMask(phase: phase).scaleEffect(3)) // this code repeats each animation iteration and in iOS 17 allocate more memory which stay there forever.
        }
    }

    struct GradientMask: View {

        let phase: CGFloat
        let centerColor = Color.black
        let edgeColor = Color.black.opacity(0.5)

        var body: some View {
            LinearGradient(
                 gradient:
                     Gradient(stops: [
                        .init(color: edgeColor, location: phase),
                        .init(color: centerColor, location: phase + 0.1),
                        .init(color: edgeColor, location: phase + 0.2)
                     ]),
                 startPoint: .topLeading,
                 endPoint: .bottomTrailing
             )
        }
    }
}

public extension View {

    /// Adds an animated skeleton effect to any view, typically to show that
    /// an operation is in progress.
    /// - Parameters:
    ///   - isActive: Convenience parameter to conditionally enable the effect.      Defaults to `true`.
    ///   - animation: A custom animation. The default animation is
    ///   `.linear(duration: 1.5).repeatForever(autoreverses: false)`.
    @ViewBuilder
      func skeleton(
        isActive: Bool = true,
        animation: Animation = .linear(duration: 1.5).repeatForever(autoreverses: false)
    ) -> some View {
        if isActive {
            redacted(reason: .placeholder)
                .modifier(SkeletonModifier(animation: animation))
        } else {
            self
        }
    }
}

Each animation repetition add approximately +5MB memory.


Solution

  • It seems to have something to do with the animation modifier in the AnimatedMak. After removing it and just setting the phase with an animation on appear seems to function properly.

        func body(content: Content) -> some View {
            content
                .modifier(
                    AnimatedMask(phase: phase)
                )
                .onAppear {
                    withAnimation(animation) {
                        phase = 0.8
                    }
                }
         }
    

    Edit: Using an implicit animation linked to the phase state makes it so that not all content gets animated

        func body(content: Content) -> some View {
            content
                .modifier(
                    AnimatedMask(phase: phase)
                )
                .animation(animation, value: phase)
                .onAppear {
                    phase = 0.8
                }
         }