Search code examples
animationswiftuiviewmodifier

SwiftUI ViewModifier animation not showing properly everytime


I have created a ViewModifier that adds a icon to the right of a its content, the way I want the icon to appear is by animating the .clipShape() modifier from -50 to 0, the problem is that when appearing the first time, it just pops out with no animation and the same thing happens when disappearing for the last time. At the bottom you'll find a video demonstration

My ViewModifier so far

extension View {
    func addRightIcon(icon: Image, show: Bool) -> some View {
        return modifier(RightIconModifier(icon: icon, show: show))
    }
}

struct RightIconModifier: ViewModifier {
    
    var icon: Image
    private var iconMask: Int = 0

    init(icon: Image, show: Bool) {
        self.icon = icon
        withAnimation(Animation.interpolatingSpring(stiffness: 170, damping: 15).delay(2.5)) {
            iconMask = show ? 0 : -50
        }
    }
    
    func body(content: Content) -> some View {
        ZStack {
            content
                .overlay(rightIcon)
        }
    }
    
    var rightIcon: some View {
        icon
            .font(.system(size: 25))
            .foregroundColor(.black)
            .frame(maxWidth: .infinity,
                   maxHeight: .infinity,
                   alignment: .trailing)
            .padding()
            .clipShape(Rectangle().offset(x: CGFloat(iconMask)))
    }
}

This would be a short version of how I'm using it, hopefully you get an idea to make it work

TextField(placeholder, text: $text).addRightIcon(icon: Image(systemName: "checkmark"), show: isTextValid)

var isTextValid: Bool {
        if !text.isEmpty {
            let validation = NSPredicate(format: "SELF MATCHES %@", "[’a-zA-Z]{3,20}")
            let validated = validation.evaluate(with: text)
            return validated
        }
        return false
    }

This is a video demonstration


Solution

  • Animatable modifiers should be inside body (directly or called from within body), but not in init. Modifier is also a struct, so if its properties modified externally they are also animatable.

    So here is fixed ViewModifier. Tested with Xcode 14 / iOS 16

    Note: I simplified animation and filter for testing purpose

    enter image description here

    struct RightIconModifier: ViewModifier {
        
        var icon: Image
        var show: Bool    // << injected changes
    
        func body(content: Content) -> some View {
            ZStack {
                content
                    .overlay(rightIcon)
            }
        }
        
        var rightIcon: some View {
            icon
                .font(.system(size: 25))
                .foregroundColor(.black)
                .frame(maxWidth: .infinity,
                       maxHeight: .infinity,
                       alignment: .trailing)
                .padding()
                .clipShape(Rectangle().offset(x: CGFloat(show ? 0 : -50))) // << switch is here !!
                .animation(.easeIn(duration: 1), // << simplified for testing
                           value: show)
        }
    }
    

    Test module on GitHub