Search code examples
swiftswiftui

Swift UI Image flickers when animating


Im trying to apply a simple shaking effect to an image to simulate a deleting animation. When I do this the image flickers. When I try this with a normal circle or any native swift ui view it works just fine. Why does the image flicker?

I have tried this with Apple's native AsyncImage and the same bug happens.

import SwiftUI
import Kingfisher

struct PinnedChatView: View {    
    var body: some View {
        ZStack {
            VStack(spacing: 8){
                KFImage(URL(string: "https://letsenhance.io/static/8f5e523ee6b2479e26ecc91b9c25261e/1015f/MainAfter.jpg"))
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
                    .jiggle(isEnabled: true)
             }
         }
    }
}
extension View {
    @ViewBuilder
    func jiggle(amount: Double = 4, isEnabled: Bool = true) -> some View {
        if isEnabled {
            modifier(JiggleViewModifier(amount: amount))
        } else {
            self
        }
    }
}

private struct JiggleViewModifier: ViewModifier {
    let amount: Double

    @State private var isJiggling = false

    func body(content: Content) -> some View {
        content
            .offset(x: isJiggling ? 3 : -3)
            .offset(y: isJiggling ? -3 : 3)
            .animation(
                .easeInOut(duration: randomize(interval: 0.07, withVariance: 0.025))
                .repeatForever(autoreverses: true),
                value: isJiggling
            )
            .animation (
                .easeInOut(duration: randomize(interval: 0.14, withVariance: 0.025))
                .repeatForever(autoreverses: true),
                value: isJiggling
            )
            .onAppear {
                isJiggling.toggle()
            }
    }

    private func randomize(interval: TimeInterval, withVariance variance: Double) -> TimeInterval {
         interval + variance * (Double.random(in: 500...1_000) / 500)
    }
}

Solution

  • Because it is animating the other modifiers like resize, aspectRatio and so on.

    A quick workaround for this is to use task modifier instead of onAppear inside the JiggleViewModifier:

    .task {
        isJiggling.toggle()
    }
    

    Demo


    Alternative method

    You can also achieve the same result with a little bit of logic change in the body of the JiggleViewModifier:

    @State private var x = 0.0
    @State private var y = 0.0
    
    func body(content: Content) -> some View {
        content
            .offset(x: x, y: y)
            .onAppear {
                let animation = Animation
                    .easeInOut(duration: randomize(interval: 0.07, withVariance: 0.025))
                    .repeatForever(autoreverses: true)
    
                withAnimation(animation) {
                    x = amount
                    y = -amount
                }
            }
    }
    

    P.S.: You originally forgot to use the amount parameter.