AnimatableModifier Inside Containers Bug

The AnimatableModifier is extremely useful. Unfortunately, it is broken if used inside a container, such as a VStack. In this page, I’ll describe a workaround.

Bug Report: FB7232586
Workaround: Yes, see below
Fixed On: iOS 13.2

Code Sample (to reproduce)

// This view works, because it is not inside a VStack
struct ContentView1: View {
    @State private var flag = false
    
    var body: some View {
        Rectangle()
            .foregroundColor(.green)
            .frame(width: 100, height: 50)
            .modifier(PctModifier(pct: self.flag ? 0 : 1))
            .onTapGesture {
                withAnimation(.easeInOut(duration: 2.0)) {
                    self.flag.toggle()
                }
        }
    }
}

// This view does not work, because it is inside a container (VStack)
struct ContentView2: View {
    @State private var flag = false
    
    var body: some View {
        VStack {
            Rectangle()
                .foregroundColor(.green)
                .frame(width: 100, height: 50)
                .modifier(PctModifier(pct: self.flag ? 0 : 1))
                .onTapGesture {
                    withAnimation(.easeInOut(duration: 2.0)) {
                        self.flag.toggle()
                    }
            }
        }
    }
}

struct PctModifier: AnimatableModifier {
    var pct: CGFloat = 0
    
    var animatableData: CGFloat {
        get { pct }
        set { pct = newValue }
    }
    
    func body(content: Content) -> some View {
        content.overlay(Text("\(Int(pct * 100))").font(.largeTitle).foregroundColor(.primary))
    }
}

There is a way to work around this problem. The code to do so, is not particularly nice, but it does the job well.

Because we cannot use .modifier(MyAnimatableModifier()) inside a VStack, what we are going to do, is put our view inside an .overlay() of a transparent view (let’s call it a placeholder). That way we can fool the framework.

The only thing left to do, is to figure out what size to set on the placeholder view. That will need to be determine case by case. In the example above, we can workaround the bug using something like this:

struct ContentView: View {
    @State private var flag = false
    
    var body: some View {
        VStack {
            Color.clear.frame(width: 100, height: 50)
                .overlay(Rectangle()
                    .foregroundColor(.green)
                    .modifier(PctModifier(pct: self.flag ? 0 : 1))
                    .onTapGesture {
                        withAnimation(.easeInOut(duration: 2.0)) {
                            self.flag.toggle()
                        }
                    }
            )
        }
    }
}

In the previous example, it’s easy because we already have a fixed size. But suppose you have an animatable modifier in a text view:

VStack {
   Text("Hello World").modifier(MyAnimatableModifier())
}

To workaround the bug, we may need something like this:

VStack {
    Text("Hello World").foregroundColor(.clear).overlay(Text("Hello World").modifier(MyAnimatableModifier()))
}

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close