Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ private struct SoftTopScrollEdge: ViewModifier {
}
}

/// Marks the hero card as the morph destination for the wallet's tapped card.
/// Without a namespace (pushed hosting, deep links) the card renders plainly.
private struct HeroCardMatch: ViewModifier {
let mint: PublicKey
let namespace: Namespace.ID?

func body(content: Content) -> some View {
if let namespace {
content.matchedGeometryEffect(id: mint, in: namespace)
} else {
content
}
}
}

struct CurrencyInfoContentV2: View {
let metadata: StoredMintMetadata
let decodedMetadata: MintMetadata
Expand All @@ -42,6 +57,12 @@ struct CurrencyInfoContentV2: View {
/// Fires when the hero card's title scrolls out from under the toolbar, so
/// the screen can reveal its own title.
let onScrolledPastTitle: (Bool) -> Void
/// False when the host already shows the card (the wallet keeps it on screen
/// while the detail rises beneath it), so it is not drawn twice.
var showsHeroCard: Bool = true
/// True while the page is animating in: the chart build and the activity
/// read are held back so they cannot stutter the transition.
var defersHeavyContent: Bool = false

private static let recentPreviewCount = 3
/// Scroll distance that puts the hero card's title row behind the toolbar.
Expand All @@ -50,18 +71,40 @@ struct CurrencyInfoContentV2: View {
private var isUSDF: Bool { metadata.mint == .usdf }
private var isOwned: Bool { viewModel.balance.hasDisplayableValue }


/// Shared with the wallet card stack when hosted as an overlay, so the
/// tapped card morphs into the hero card above.
@Environment(\.walletCardNamespace) private var cardNamespace

var body: some View {
ScrollView {
// Horizontal insets are applied per section rather than to the whole
// stack: the market-cap chart bleeds edge to edge, and cancelling an
// outer inset with negative padding makes that row wider than the
// viewport, which turns the scroll view horizontally scrollable.
VStack(spacing: 24) {
heroCard
.padding(.horizontal, 20)
// The slot is always reserved: while the wallet's card is still
// in the air this is empty, but dropping it from the layout
// shifts everything below up and then jumps it back down when
// the card lands.
Group {
if showsHeroCard {
heroCard
} else {
Color.clear.frame(height: WalletCardGeometry.cardHeight)
}
}
.padding(.horizontal, 20)
actionTiles
.padding(.horizontal, 20)

// Everything below is laid out and shown from the first frame,
// so the page arrives complete rather than in stages. Nothing
// here is gated on loading: the activity read is quick and runs
// off the main thread, and the market-cap section reserves its
// full height around a placeholder plot. Revealing these later —
// even all at once — reads as the tiles landing and the rest of
// the screen following a beat behind.
if isOwned && !viewModel.recentActivities.isEmpty {
recentSection
.padding(.horizontal, 20)
Expand All @@ -74,7 +117,8 @@ struct CurrencyInfoContentV2: View {
CurrencyInfoMarketCapSection(
marketCap: viewModel.marketCap,
currencyCode: ratesController.balanceCurrency,
marketCapController: marketCapController
marketCapController: marketCapController,
isReady: !defersHeavyContent
)
}

Expand Down Expand Up @@ -104,6 +148,10 @@ struct CurrencyInfoContentV2: View {
} action: { _, scrolledPast in
onScrolledPastTitle(scrolledPast)
}
// Started immediately rather than after the transition: the read itself
// runs off the main thread, so it costs the animation nothing, and it
// lands well before the page finishes fading in — which is what keeps
// Recent from arriving as a second stage.
.task {
await viewModel.loadRecentActivities(limit: Self.recentPreviewCount)
}
Expand All @@ -113,6 +161,9 @@ struct CurrencyInfoContentV2: View {

private var heroCard: some View {
TokenCardView(data: heroData, height: 224)
// Destination of the wallet card's morph when hosted as an overlay,
// so the tapped card becomes this one rather than cross-fading.
.modifier(HeroCardMatch(mint: metadata.mint, namespace: cardNamespace))
}

private var heroData: TokenCardData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ struct CurrencyInfoMarketCapSection: View {
let marketCap: FiatAmount
let currencyCode: CurrencyCode
let marketCapController: MarketCapController
/// Gates the chart's *data* only. The section itself — its value, the
/// all-time change, and the range picker — renders immediately around a
/// reserved 200pt plot area, so the page arrives complete and in its final
/// layout. Drawing an actual populated Swift Charts plot is the expensive
/// part, and the host holds that back until the opening animation is done.
var isReady: Bool = true

var body: some View {
VStack(alignment: .leading) {
Expand All @@ -34,9 +40,17 @@ struct CurrencyInfoMarketCapSection: View {
}
.padding(.top, 20)
.padding(.bottom, 20)
// The view model is cheap and gives the section its full height right
// away: value, change, a placeholder plot, and the range picker.
.task {
guard chartViewModel == nil else { return }
setupChart()
}
// The points, and so the real plot, come once the host says it is safe.
.task(id: isReady) {
guard isReady, let viewModel = chartViewModel else { return }
loadChartData(for: viewModel.selectedRange, into: viewModel)
}
.onChange(of: marketCap) { _, newMarketCap in
// Live ticks only move the appended "current" point — history
// doesn't change when the spot value moves, so no refetch.
Expand All @@ -51,10 +65,10 @@ struct CurrencyInfoMarketCapSection: View {

private func setupChart() {
let viewModel = ChartViewModel(currentValue: marketCap.doubleValue, selectedRange: .all)
viewModel.setLoading()
chartViewModel = viewModel

updateRangeChangeCallback(for: viewModel)
loadChartData(for: .all, into: viewModel)
}

private func updateRangeChangeCallback(for viewModel: ChartViewModel) {
Expand Down
Loading