1

I need some help. I created the following reusable view, let's call it MyCustomCapsuleView:

enter image description here

Nothing special, just a Text with a Capsule in the background. This view will have a different look when the user turns it on.

enter image description here

When the user turns it on, MyCustomCapsuleView padding should increase to 17 to account for the extra space needed for the new look. Also, it should:

  1. Scale up and back
  2. Start wiggling

When the user turns it off, MyCustomCapsuleView padding should go back to 3, as the extra room is not needed anymore. Also, it should:

  1. Stop wiggling
  2. Scale up and down, again

I was able to get a functional version of this, but the paddings are not working properly. When MyCustomCapsuleView is wiggling, the paddings go back and forth between 3 and 17 (see the yellow background).

enter image description here

  1. Any ideas what am I doing wrong? How can I achieve the behaviour I need? Note: Since this view is reusable, I need that the "turned off" version of my view keeps padding = 3. I will use it across different parts of my app and only in some places the "turned on/off" version will be available, and only in those places the padding for my view is 17.

  2. Why the rotation (wiggle effect) looks so weird? I tried different anchor unit poi.ts (center, trailing, etc) and none of them seems to give me a good look.

Thanks for your help!

Here's my code:

struct MyCustomCapsuleView: View {
    @State private var scaleUp = false
    
    var showRemove: Bool
    private var fillColor: Color { return Color(#colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1)) }
    private var foregroundColor: Color { return Color(#colorLiteral(red: 0.1019607857, green: 0.2784313858, blue: 0.400000006, alpha: 1)) }
    
    var body: some View {
        ZStack(alignment: .topTrailing) {
            Text("Hello World!")
                .foregroundColor(foregroundColor)
                .padding(.horizontal)
                .padding(.vertical, 10)
                .lineLimit(1)
                .background(
                    ZStack {
                        Capsule(style: RoundedCornerStyle.continuous)
                            .fill(fillColor)
                        Capsule(style: RoundedCornerStyle.continuous)
                            .stroke(foregroundColor, lineWidth: 2)
                    }
                )
            
            Circle()
                .fill(Color.red)
                .frame(width: 30, height: 30)
                .overlay(
                    Image(systemName: "multiply")
                        .resizable()
                        .padding(7)
                        .foregroundColor(.white)
                )
                .offset(x: 15, y: -15)
                .scaleEffect(showRemove ? 1 : 0, anchor: UnitPoint.topTrailing)
                .animation(.spring(response: 0.3, dampingFraction: 0.6, blendDuration: 0), value: showRemove)
        }
        .padding(showRemove ? 17: 3)
        .background(Color.yellow)
        .scaleEffect(scaleUp ? 1.2 : 1)
        .animation(.spring(response: 0.3, dampingFraction: 0.6, blendDuration: 0), value: scaleUp)
        .rotationEffect(.degrees(showRemove ? 7 : 0), anchor: UnitPoint(x: 0.10, y: 0.0))
        .animation(showRemove ? .easeInOut(duration: 1.15).repeatForever(autoreverses: true).delay(0.3) : .easeInOut(duration: 0.15), value: showRemove)
        .onChange(of: showRemove, perform: { value in
            if !scaleUp {
                scaleUp = true
                Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in
                    scaleUp = false
                }
            }
        })
    }
}

struct Test: View {
    @State var showClose = false
    
    var body: some View {
        HStack {
            MyCustomCapsuleView(showRemove: showClose)
            Button("Turn On/Off") {
                showClose.toggle()
            }
        }
    }
}
George
  • 25,988
  • 10
  • 79
  • 133
Aнгел
  • 1,361
  • 3
  • 17
  • 32

1 Answers1

0

This is kind of tricky. Currently, your .repeatForever(autoreverses: true) animation is animating not only the rotationEffect, but also the padding. To prevent this and use the repeatForever only for rotationEffect, I think the way to go is with explicit animations and 2 separate @States.

  • Explicit animations let you specify which properties to animate.
  • 2 separate @States allows a different animation for each var.
    • @State var showClose is a one-time animation for creating a larger padding() and the scale-up animation
    • @State var rotating is the repeatForever animation for the rotation

Here's what your Test struct would look like:

struct Test: View {
    @State var showClose = false
    @State var rotating = false
    
    var body: some View {
        HStack {
            MyCustomCapsuleView(showRemove: showClose, rotating: rotating)
            
            Button("Turn On/Off") {
                print(showClose)
                withAnimation(.spring(response: 0.3, dampingFraction: 0.6, blendDuration: 0)) {
                    showClose.toggle()
                }
                withAnimation(rotating ? .easeInOut(duration: 0.15) : .easeInOut(duration: 1.15).repeatForever(autoreverses: true).delay(0.3)) {
                    rotating.toggle()
                }
            }
        }
    }
}

Then, modify these lines in your MyCustomCapsuleView:

var showRemove: Bool
var rotating: Bool /// add this
/// replace your `ZStack`'s modifiers with this:
.padding(showRemove ? 17: 3)
.background(Color.yellow)
.scaleEffect(scaleUp ? 1.2 : 1)
.rotationEffect(.degrees(rotating ? 7 : 0))
.onChange(of: showRemove, perform: { value in
    if !scaleUp {
        scaleUp = true

        /// btw, why not just `DispatchQueue.main.asyncAfter(.now() + 0.1)`?
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in
            scaleUp = false
        }
    }
})

Result:

Animation as expected, first gets larger, then rotates around center

aheze
  • 24,434
  • 8
  • 68
  • 125