Block · Account
NewPlan and Usage SwiftUI Block
The subscription section of Settings: the plan and its renewal, usage meters that warn near a limit, and the way up.
Where a subscriber checks their plan. A summary card names the plan with a tinted status chip, its price and when it renews, ends or leaves its trial; usage meters fill to what has been used and turn to warning and danger near a limit. Upgrade works, confirms and folds away on the top plan, and Manage subscription and Restore purchases use StoreKit by default.
- SwiftUI
- iOS 17.0+
- plan-usage
$199 once, every piece included. See how installing works · Sign in
01
What's included
SwiftUI source you own, built on Apple frameworks only.
- The plan with a status chip, price and renewal date
- Usage meters that warn near a limit and show when allowances reset
- Upgrade with working, done and failed shown in place
- Manage subscription and Restore purchases through StoreKit
- Loading, payment issue and free plan states from one
PlanUsage
Requires
In-App Purchase capability
02
Implementation notes
How it behaves, what it handles, and the decisions behind it.
A summary card names the plan, with a tinted status chip and, under a hairline, its price and the date it renews, ends or leaves its trial; a payment issue turns that line to the danger colour. The Usage card holds a meter for each limit: a symbol and label, the figure ("8.2 of 10 GB") and a thin bar that fills when the block appears and moves whenever you pass a new value. A meter that passes its warning share fills first, then turns to the warning colour and says "Almost full"; at the limit it turns to the danger colour and says "Limit reached". Allowances read "Resets Nov 2" under the bar. Upgrade carries the plan's own call to action with what you get under it, shows progress while your closure runs and a checkmark when it returns, then folds away once you pass the top plan, while the summary and meters settle on the new one. A thrown error shows its message under the button. Manage subscription opens the App Store's own sheet, and Restore purchases runs AppStore.sync() with its working, done and failed states. Pass nil while billing loads and the block shows grey shapes in its own layout.
Notes
PlanUsageBlock(usage:onUpgrade:onManage:onRestore:)takes onePlanUsageand the upgrade closure. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your own column: aScrollViewwith.padding(.horizontal, 20), or a sheet.PlanUsageholds the plan's name, itsPrice(.amount(9.99, currencyCode: "USD"),.display(product.displayPrice)or.free), thePeriod(.week,.month,.year, or nil), aStatus, the meters, theUpgradeand an optionalsubscriptionGroupID.PlanUsage.sampleis the Plus plan from the preview.Statussets the chip and the date line:.renews(date)("Active", "Renews Nov 2"),.trial(ends:)("Free trial", "Trial ends Nov 2"),.ends(date)("Cancelled", "Ends Nov 2"),.paymentIssue(until:)("Payment issue", "Update payment by Nov 2" in the danger colour) and.noRenewalfor free plans and one-time purchases, which shows no chip and hides Manage subscription. A free plan with nothing to renew leaves the price line out.- A
PlanUsage.Meterhas anid,label,used,limit,unit("GB", or empty for counts), aformatfor both figures, an optional SF Symbol andwarnAt(0.85 by default). Give it aresetsdate and it reads as an allowance: "Resets Nov 2" under the bar, then "Running low" and "All used" instead of "Almost full" and "Limit reached".usedmay pass the limit.fractionandlevel(.normal,.nearLimit,.atLimit) are there for your own logic. PlanUsage.Upgradeis the button'stitle("Upgrade to Studio") and an optionaldetailline of what you get. Nil on the top plan.onUpgradeisasync throws: present your paywall or callproduct.purchase()there, then hand the block the newPlanUsage. The button shows a spinner until it returns, then a checkmark for a moment. Throwing shows the error'serrorDescription(or "The upgrade didn't go through. Try again.") under the button in the danger colour; throwCancellationError()when the person backs out and the button goes back to rest quietly.- Without
onManagethe block presentsmanageSubscriptionsSheet, onsubscriptionGroupIDwhen you set one. PassonManagefor plans billed outside the App Store; then the row also shows on plans that don't renew. - Without
onRestorethe block callsAppStore.sync(). A cancelled App Store sign-in goes back to rest quietly; any other error shows its description or "Couldn't reach the App Store. Try again." Done shows "Purchases restored" for a few seconds. Your app's transaction listener picks up whatever comes back. - Haptics mark success when an upgrade or restore finishes and an error when either fails.
- VoiceOver reads the summary as one element ("Plus plan, $9.99 per month. Active. Renews Nov 2"), each meter as its label with the figure, the percentage and its line, and announces upgrade and restore results. Text grows with Dynamic Type: rows stack their figures under their labels, the chip moves under the name, and symbols step aside at accessibility sizes so text keeps the width.
- Reduce Motion places the meters at their values without the fill, and state changes become short fades.
PlanUsageBlockExampleat the end ofPlanUsageBlock.swiftis the Account screen in the preview: Plus two weeks before it renews, storage climbing into its warning as a backup finishes, and an upgrade to Studio that settles the plan and meters on the new tier. It reads the theme from the environment; passwrapsInNavigationStack: falseto push it in your own stack.
Theming
- Reads
SPThemefrom@Environment(\.spTheme)and requires thedesign-systemitem. - Cards are
color.surfacewithradius.lg, 16 point padding andSPHairlinedividers inseparator, inset to the text; groups sit 24 points apart (space.xl). - The plan's name is the one display moment: the brand's display face (
type.face) at 28 points,type.displayWeightcapped at semibold, intextPrimary. When the face isn't bundled it falls back to SF intype.figureDesign, or intype.textDesignwhen figures are monospaced, so a word is never set in SF Mono. Labels and rows usetype.body, figurestype.subheadwith monospaced digits, secondary linestype.footnote, the Usage titletype.headline. - Text is
textPrimaryandtextSecondary; symbols aretextSecondary, the Manage arrowtextTertiary. - Bars run in
textPrimaryon afilltrack, turning towarninganddanger. The status chip is its status colour (success,info,danger) on a 14% tint of it, ortextSecondaryonfillwhen cancelled. Restore's done line issuccess, failuresdanger. - The upgrade button is the
accentwithaccentOntext intype.headlineandradius.md, the only accent on the block. - Fills move with
motion.heroon appear andmotion.smoothon change; states cross-fade withmotion.snappy. Every animation becomes a short fade under Reduce Motion.
Requirements
- In-App Purchase capability, for the manage sheet and
AppStore.sync(). Neither is needed when you passonManageandonRestore.
03
Keep building
Pieces that work well next to this one.