Skip to content

feat(wallet): expand token cards into the currency info screen - #586

Closed
bmc08gt wants to merge 1 commit into
feat-currency-info-v2from
feat-wallet-zoom-transition
Closed

feat(wallet): expand token cards into the currency info screen#586
bmc08gt wants to merge 1 commit into
feat-currency-info-v2from
feat-wallet-zoom-transition

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Tapping a bill card pulls it out of the deck and into the currency info screen, Apple Wallet style. Closing puts it back in its slot, sliding under the cards stacked on top of it.

How it works

The deck is driven by one expansionProgress scalar that the card's flight animates in lockstep with — the cards above gather onto the slot it rises to and fade underneath it, the ones below run off the bottom, and the balance header fades with them. The opened card keeps its natural place in the deck's z-order the whole way, which is what makes closing read as reinserting rather than dropping it on top.

Things worth knowing before changing it

Most of these were bugs first, and are easy to reintroduce:

  • The reorganisation is a value, not a branch. visualEffect interpolates a changing offset, but swapping which formula applies (by toggling the opened mint) resolves in a single frame and reads as a snap.
  • Blocking work goes before the commit, not after. Hiding the tab bar relayouts the whole TabView, and building the page runs a synchronous metadata read. Both happen in the un-animated seed step. SwiftUI's springs are driven on the main thread, so work deferred until after withAnimation interrupts the motion rather than running alongside it — measured as worse, not better. The tab bar is restored the same way at the top of the close, or it arrives visibly late.
  • The hand-off waits on the animation, not a sleep. A spring is still settling when its nominal duration is up; swapping the flying card for the page's hero card on a timer left them 16px apart and juddered. The page is also non-interactive until that hand-off — scrolling in the gap slides content out from under a card that isn't in the scroll view yet.
  • Callbacks are tagged. Both transitions finish in callbacks that outlive them. Untagged, a close settling in the background clears state out from under the open that interrupted it, leaving the wallet with its header faded out and the tab bar stuck hidden.
  • The screen arrives in one pass. Nothing below the tiles is gated on loading — the activity read is off the main thread, and the market cap section builds its view model up front so it holds full height around a placeholder plot. Only the chart's points wait, since a populated Swift Charts draw is the genuinely expensive part.
  • Overlay hosting draws its own top fade. The system's soft scroll edge is rendered by the navigation bar's background, and an overlay has none.

Testing

Driven with Maestro and verified from frame-by-frame analysis of screen recordings rather than by eye:

  • Open/close on a top card, a mid card, and one deep in the deck
  • Card's top edge tracked per frame — lands with no snap-back, eases to rest in 2px steps
  • Dropped frames during the opening: 2–4 of 26 (Debug, simulator)
  • 30 back-to-back open/close cycles with no settle time — header and tab bar intact after each
  • Scroll immediately after opening, with the transition slowed to widen the window: reproduced the stranded card, then confirmed the fix removes it
  • Full flow: open → scroll → back → assert dismissed

Tapping a bill card in the wallet now pulls it out of the deck and into the
currency info screen, Apple Wallet style, and closing puts it back where it
came from.

The deck is driven by a single `expansionProgress` scalar that the card's
flight animates in lockstep with: the cards above gather onto the slot it
rises to and fade underneath it, the ones below run off the bottom, and the
balance header fades with them. The opened card keeps its natural place in
the deck's z-order throughout, which is what lets it slide back *under* its
neighbours on the way home rather than landing on top of them.

Notes on the parts that are less obvious than they look:

- The reorganisation has to be a value, not a branch. `visualEffect`
  interpolates a changing offset, but swapping which formula applies (by
  toggling the opened mint) resolves in one frame and reads as a snap.
- Work that blocks the main thread — hiding the tab bar, which relayouts the
  whole `TabView`, and building the page — happens in an un-animated step
  before the animation is committed. SwiftUI's springs are driven on the main
  thread, so anything deferred until after the commit interrupts the motion
  instead of running alongside it. The tab bar is restored the same way, at
  the top of the close, so it does not arrive late.
- The hand-off to the page's hero card fires on the animation's completion
  rather than a matching sleep: a spring is still settling when its nominal
  duration is up, and swapping then leaves the two cards a few points apart.
  The page also stays non-interactive until that hand-off, or a scroll begun
  in the gap slides the content out from under a card that is still the copy
  flying above it.
- Both transitions finish their work in callbacks, so each is tagged and
  every callback checks it is still current — otherwise a close settling in
  the background tears down the open that interrupted it.

The screen itself arrives in one pass. Nothing below the action tiles is
gated on loading: the activity read runs off the main thread, and the market
cap section builds its view model up front so it holds its full height around
a placeholder plot. Only the chart's points, and so the expensive populated
Swift Charts draw, wait for the transition to finish.

Hosted as an overlay the screen has no navigation bar, so it draws its own
chrome and its own top fade — the system's soft scroll edge is rendered by
the bar's background, and without it the page scrolled up to meet the status
bar at full strength.
@bmc08gt

bmc08gt commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #587 — rebased onto main. GitHub closed this when #585 merged and feat-currency-info-v2 was deleted, rather than retargeting it.

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.

1 participant