Search code examples
iosxcodeswiftuiuibezierpathswiftui-animation

How can I animate changes to a BezierPath defined custom cornerRadius with SwiftUI?


I am using the following approach to add rounded corners to a view on x number of corners:

Round Specific Corners SwiftUI

extension View {
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape( RoundedCorner(radius: radius, corners: corners) )
    }
}

struct RoundedCorner: Shape {

    var radius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners

    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        return Path(path.cgPath)
    }
}

This works very well. Unfortunately when I animate this view to another Frame without any rounded corners there is no animation for the cornerRadius. All other animations work fine.

To Illustrate this, the following shows the corner radius animation with the standard .cornerRadius modifier and with the custom .cornerRadius modifier using the extension above:

struct ContentView: View {
    
    @State var radius: CGFloat = 50

    var body: some View {
        VStack {
            Button {
                withAnimation(.easeInOut(duration: 2)) {
                    if radius == 50 {
                        radius = 0
                    } else {
                        radius = 50
                    }
                }
                
            } label: {
                Text("Change Corner Radius")
            }

            Color.red
                .frame(width: 100, height: 100)
                .cornerRadius(radius, corners: [.topLeft, .bottomRight])
            
            Color.blue
                .frame(width: 100, height: 100)
                .cornerRadius(radius)
        }
    }
}

enter image description here


Solution

  • The issue is in the RoundedCorner struct. It was not written with animations in mind. While a struct conforming to the Shape protocol is animatable, it won't animate without a var animatableData in it that provides the ability for the system to understand how to animate the Shape. I don't know why it isn't required to be implemented, because it is usually pretty trivial to do, as in this case.

    Change your RoundedCorner struct to the following, and it will animate like you want it to:

    struct RoundedCorner: Shape {
    
        var radius: CGFloat
        var corners: UIRectCorner
        var animatableData: CGFloat {
            get { return radius }
            set { radius = newValue }
        }
    
        func path(in rect: CGRect) -> Path {
            let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
            return Path(path.cgPath)
        }
    }
    

    enter image description here