Skip to content

refactor: spring-in, fade-out toast motion#592

Merged
pwltr merged 3 commits into
synonymdev:masterfrom
CypherPoet:refactor/toast-spring-animation
Jun 12, 2026
Merged

refactor: spring-in, fade-out toast motion#592
pwltr merged 3 commits into
synonymdev:masterfrom
CypherPoet:refactor/toast-spring-animation

Conversation

@CypherPoet

@CypherPoet CypherPoet commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Description

This PR aims to establish a more-polished motion design for the app's toast show/hide motion, which previously animated with a flat ease (easeInOut, 0.4s) that felt a tad mechanical compared to the spring motion used across the rest of the app (including the toast's own drag-to-dismiss snap-back).

I went with a behavior that's asymmetric, mirroring Apple's system banner pattern:

  1. Arrival is physical: the toast materializes in place, settling down from 12pt above its resting position with an opacity fade, and a slight 0.98 → 1.0 scale, driven by .snappy(duration: 0.4) (a gently damped spring preset). This replaces the full slide-in from offscreen, which swept the whole banner across the top of the screen.
  2. Departure gets out of the way: a quick fade (easeOut, 0.2s). By dismissal time the toast has been read, so exit motion is kept to a minimum.
  3. The drag-to-dismiss snap-back now uses the same entrance curve, so the component speaks one motion dialect.

The motion constants live in a single ToastMotion namespace. And I wanted to make them a convenient starting point for any feedback from the design team:

Knob Value
Entrance curve .snappy(duration: 0.4)
Settle distance (from above) 12pt
Entrance scale 0.98 → 1.0
Exit curve .easeOut(duration: 0.2)

Note on cross-platform consistency: Android currently slides the full toast in and out from the top edge with a fade, symmetric in both directions (fadeIn() + slideInVertically / fadeOut() + slideOutVertically in ToastView.kt), and its drag snap-back uses a 0.7-damping spring, the same entrance/gesture mismatch this PR addresses on iOS. If this PR goes forward, a counterpart Android PR maps 1:1 (fadeIn + slideInVertically(-12dp) + scaleIn(0.98) in, fadeOut out, snap-back damping aligned), and I can take that on as a follow-up.

Linked Issues/Tasks

Follow-up to the motion conversation in #585: #585 (comment)

Screenshot / Video

ios-transfer-amount-exceeded-toast-side-by-side.mp4

Feels a bit less like the toast is being rammed into your face, IMO 😂 ... also plays nicely with "standard", informational toast messages:

ios-toast-motion-side-by-side.mp4

QA Notes

Manual Tests

  • 1. Settings → Advanced → Address Viewer → long-press an address: Copied toast settles in from just above, auto-hides after 4s with a quick fade.
  • 2. regression: toast visible → tap content beneath the toast: taps pass through, and the area is tappable again after dismiss.
  • 3a. regression: drag the toast upward: dismisses.
    • 3b. small downward drag → release: snaps back and auto-hide resumes.

Automated Checks

  • No automated coverage exists for toast animation; verified by simulator recording (before/after videos above).
  • CI: standard build and test checks run by the PR bot.

Toast show/hide previously animated with a flat easeInOut(0.4) that read
as mechanical next to the spring motion used across the rest of the app,
including the toast's own drag-to-dismiss snap-back.

Make the motion asymmetric, mirroring Apple's system banner pattern:
the toast materializes in place (12pt rise, opacity fade, 0.98 scale)
with .snappy(duration: 0.4), and departs with a quick easeOut fade.
The drag snap-back joins the same motion family, and the blanket
.animation(_:value:) modifier is removed since it would force both
directions onto one curve. Constants live in a ToastMotion namespace
so the design team has one place to tune.
@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR refactors the toast show/hide animation from a symmetric easeInOut(0.4s) slide to an asymmetric spring entrance (.snappy(0.4s) with a 12 pt offset, opacity, and 0.98→1.0 scale) and a plain easeOut(0.2s) fade-out, unifying all toast motion under a single ToastMotion namespace. The drag-to-dismiss snap-back is also updated to reuse the entrance curve for consistency.

  • ToastMotion namespace centralises all animation constants (entrance, exit, exitSettleTime) in ToastWindowManager.swift, replacing inline animation literals across showToast, hideToast, scheduleAutoHide, and ToastView's snap-back.
  • Asymmetric .transition replaces the previous .move(edge: .top).combined(with: .opacity) with a subtle "settle from above" entrance and an opacity-only exit; the view-level .animation(_:value:) modifier is removed in favour of explicit withAnimation transactions so each direction uses its own curve.
  • exitSettleTime (0.3 s) is intentionally larger than the exit animation duration (0.2 s), but the two values are decoupled with no enforced ordering — see inline comment.

Confidence Score: 4/5

Safe to merge — the changes are scoped entirely to animation parameters with no logic, data, or networking paths affected.

The refactor is clean and well-reasoned. The two concerns are both minor: exitSettleTime and the exit animation duration are separate constants that must be manually kept in order, and the offset direction in the insertion transition is described inconsistently in prose vs. code. Neither affects correctness today, but the decoupled timing constants could silently cause hit-test issues if exit is later lengthened past exitSettleTime.

The ToastMotion namespace in ToastWindowManager.swift — specifically the relationship between exit animation duration and exitSettleTime.

Important Files Changed

Filename Overview
Bitkit/Managers/ToastWindowManager.swift Adds the ToastMotion namespace and replaces all animation literals with its constants; removes the view-level .animation(_:value:) modifier in favour of explicit withAnimation transactions; introduces an asymmetric .transition (spring-offset+scale+opacity in, opacity-only out). Two P2 observations: exitSettleTime is decoupled from exit.duration with no enforced ordering, and the offset direction comment is semantically reversed.
Bitkit/Components/ToastView.swift Single-line change: snap-back animation swapped from an inline .spring(response:dampingFraction:) to ToastMotion.entrance, unifying the motion dialect between entrance and drag-release. No logic changes; safe.
changelog.d/next/592.changed.md New changelog entry describing the animation change; no code impact.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant ToastWindowManager
    participant ToastWindowView
    participant PassThroughWindow

    Note over Caller,PassThroughWindow: Toast Show
    Caller->>ToastWindowManager: showToast(_:)
    ToastWindowManager->>PassThroughWindow: "hasToast = true"
    ToastWindowManager->>ToastWindowView: "withAnimation(ToastMotion.entrance) currentToast = toast"
    ToastWindowView-->>ToastWindowView: .asymmetric insertion (offset -12pt + opacity + scale 0.98→1.0) driven by .snappy(0.4s)
    ToastWindowManager->>ToastWindowManager: scheduleAutoHide(after: visibilityTime)

    Note over Caller,PassThroughWindow: Auto-hide / Manual hide
    ToastWindowManager->>PassThroughWindow: "hasToast = false"
    ToastWindowManager->>ToastWindowView: "withAnimation(ToastMotion.exit) currentToast = nil"
    ToastWindowView-->>ToastWindowView: .asymmetric removal (opacity only) driven by .easeOut(0.2s)
    ToastWindowManager->>ToastWindowManager: "Task.sleep(exitSettleTime = 0.3s)"
    ToastWindowManager->>PassThroughWindow: "toastFrame = .zero"

    Note over Caller,PassThroughWindow: Drag snap-back
    ToastWindowView->>ToastWindowView: "withAnimation(ToastMotion.entrance) dragOffset = 0 (.snappy 0.4s)"
Loading

Reviews (1): Last reviewed commit: "refactor: rework toast motion to spring ..." | Re-trigger Greptile

Comment thread Bitkit/Managers/ToastWindowManager.swift Outdated
Comment thread Bitkit/Managers/ToastWindowManager.swift
Addresses automated review feedback: the settle time used for hit-test
frame cleanup is now computed from the exit animation's duration instead
of relying on a comment-enforced invariant, and the transition comment
now correctly describes the entrance as settling down from above rather
than rising.
Comment thread Bitkit/Components/ToastView.swift Outdated
@pwltr pwltr requested a review from aldertnl June 11, 2026 12:29
aldertnl
aldertnl previously approved these changes Jun 11, 2026

@aldertnl aldertnl left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@pwltr pwltr enabled auto-merge (squash) June 12, 2026 08:06
@pwltr pwltr merged commit 7256636 into synonymdev:master Jun 12, 2026
19 of 28 checks passed
@CypherPoet CypherPoet deleted the refactor/toast-spring-animation branch June 12, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants