1

How can I get the blue circles to first move away from the green circle before getting back to it?

The animation should be:

  • press and hold:
    • the green circle scales down
    • the blue circles, while scaling down as well, first move "up" (away from their resting position, as if pushed away by the pressure applied) and then down (to touch the green circle again, as if they were pulled back by some gravitational force)
  • release
    • everything springs back into place
    • (bonus) ideally, the blue circles are ejected as the green circle springs up, and they fall back down in their resting position (next to the surface)

I got everything working except the blue circles moving up part.

This is the current animation: enter image description here

And its playground.

import SwiftUI
import PlaygroundSupport

struct DotsView: View {
    var diameter: CGFloat = 200
    var size: CGFloat = 25
    var isPressed: Bool = false

    var body: some View {
        ZStack {
            ForEach(0...5, id: \.self) { i in
                Circle()
                    .fill(Color.blue)
                    .frame(width: size, height: size)
                    .offset(x: 0, y: -(diameter)/2 - size/2)
                    .rotationEffect(.degrees(CGFloat(i * 60)))
            }
        }
        .frame(width: diameter, height: diameter)
        .animation(.none)
        .scaleEffect(isPressed ? 0.8 : 1)
        .animation(
            isPressed ? .easeOut(duration: 0.2) : .interactiveSpring(response: 0.35, dampingFraction: 0.2),
            value: isPressed
        )
        .background(
            Circle()
                .fill(Color.green)
                .scaleEffect(isPressed ? 0.8 : 1)
                .animation(isPressed ? .none : .interactiveSpring(response: 0.35, dampingFraction: 0.2), value: isPressed)
        )
    }
}

struct ContentView: View {
    @State private var isPressed: Bool = false

    var body: some View {
        DotsView(
            diameter: 200,
            isPressed: isPressed
        )
        .frame(width: 500, height: 500)
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
                .onChanged { _ in
                    isPressed = true
                }
                .onEnded { _ in
                    isPressed = false
                }
        )
    }
}

let view = ContentView()
PlaygroundPage.current.setLiveView(view)

Thanks

Arnaud
  • 17,268
  • 9
  • 65
  • 83
  • SceneKit and/or SpriteKit are likely better for this. For me SwiftUI hits a capabilities limit when you start combining animations. – lorem ipsum Jul 04 '22 at 14:57
  • @loremipsum I was afraid you were right, but learned thanks Asperi's answer that GeometryEffect is made precisely for that: go beyond the default capabilities and teach SwiftUI how to animate your objects. Of course, it doesn't work for everything, but in this instance, it gets the job done. – Arnaud Jul 05 '22 at 05:53

1 Answers1

1

Actually all is needed is to replace linear scaleEffect with custom geometry effect that gives needed scale curve (initially growing then falling).

Here is a demo of possible approach (tested with Xcode 13.4 / iOS 15.5)

demo

Main part:

struct JumpyEffect: GeometryEffect {
    let offset: Double
    var value: Double

    var animatableData: Double {
        get { value }
        set { value = newValue }
    }

    func effectValue(size: CGSize) -> ProjectionTransform {
        let trans = (value + offset * (pow(5, value - 1/pow(value, 5))))
        let transform = CGAffineTransform(translationX: size.width * 0.5, y: size.height * 0.5)
            .scaledBy(x: trans, y: trans)
            .translatedBy(x: -size.width * 0.5, y: -size.height * 0.5)
        return ProjectionTransform(transform)
    }
}

and usage

.modifier(JumpyEffect(offset: isPressed ? 0.3 : 0, value: isPressed ? 0.8 : 1))

Complete code on GitHub

Asperi
  • 228,894
  • 20
  • 464
  • 690