ARTICLE AD BOX
I am trying to replicate the Robinhood app's "Swipe up to confirm" animation in SwiftUI..
Basically a green bar at the bottom of the screen that the user drags upwards.
As they drag, the bar follows the finger.
Once it passes a certain threshold, the bar should smoothly snap to fill the entire screen and trigger a confirmation state.
The animation partially works, but there is a noticeable "freeze" or stutter about half way, it hangs for a few seconds.
This is a good video of what I'm trying to accomplish.
struct HomeIncomeFinishedView: View { @State private var isConfirmed = false var body: some View { ZStack(alignment: .bottom) { // MARK: - Background Layer VStack { VStack { if isConfirmed { Color.black } else { Text("Hello World") .font(.largeTitle) .foregroundColor(.white) .padding(.top, 60) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .background(Color.black) .cornerRadius(isConfirmed ? 0 : 30) // Remove corners if confirmed Spacer().frame(height: 160) } // MARK: - Slide Up Component SlideUpSectionView(isConfirmed: $isConfirmed) .zIndex(1) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.black) .ignoresSafeArea() } } struct SlideUpSectionView: View { @Binding var isConfirmed: Bool // Configuration private let defaultHeight: CGFloat = 160 private let threshold: CGFloat = 150 @State private var dragOffset: CGFloat = 0 var body: some View { GeometryReader { geometry in VStack(spacing: 0) { Spacer(minLength: 0) // The Interactable Green Area VStack(spacing: 0) { if isConfirmed { // MARK: - Confirmed Content VStack(spacing: 20) { Spacer() ProgressView() .tint(.white) .scaleEffect(1.5) Text("Order Received") .font(.title3) .fontWeight(.bold) .foregroundColor(.white) Spacer() } .padding(.top, 60) .transition(.opacity) } else { // MARK: - Swipe Handle Content VStack(spacing: 4) { Image(systemName: "chevron.up") .font(.title3) .fontWeight(.bold) .offset(y: -dragOffset * 0.1) Text("Swipe up to confirm") .font(.headline) .fontWeight(.bold) } .foregroundColor(.black) .opacity(calculateOpacity()) .padding(.top, 20) .padding(.bottom, 50) // Adjust based on home indicator } } .frame(maxWidth: .infinity) .frame(height: isConfirmed ? nil : defaultHeight + dragOffset) .frame(maxHeight: isConfirmed ? .infinity : nil) .background(Color.green) .mask { if isConfirmed { Rectangle() } else { UnevenRoundedRectangle( topLeadingRadius: 20, bottomLeadingRadius: 0, bottomTrailingRadius: 0, topTrailingRadius: 20 ) } } .gesture( isConfirmed ? nil : DragGesture() .onChanged { value in let translation = -value.translation.height if translation > 0 { withAnimation(.interactiveSpring()) { dragOffset = translation } } } .onEnded { value in let translation = -value.translation.height if translation > threshold { confirmAction() } else { resetAction() } } ) } .ignoresSafeArea() } } private func confirmAction() { let generator = UIImpactFeedbackGenerator(style: .heavy) generator.impactOccurred() withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { isConfirmed = true dragOffset = 0 } } private func resetAction() { withAnimation(.spring(response: 0.4, dampingFraction: 0.6)) { dragOffset = 0 } } private func calculateOpacity() -> Double { let progress = dragOffset / threshold return Double(1 - progress) } }