Block · Commerce

New

Checkout SwiftUI Block

A SwiftUI checkout screen. A one-column checkout: bag with steppers, address and delivery speed, promo code, summary, then Pay or Apple Pay to the placed order.

The checkout section of a cart or checkout screen. Each bag line has a quantity stepper, and the line price, summary and Pay label roll as quantities change; delivery shows the address with Change and each speed with its day and price, and a promo code applies or explains why not. Pay shows its working, done and failed states in place, Apple Pay uses the system button, and the block settles into the placed order with its number and arrival day.

  • SwiftUI
  • iOS 17.0+
  • checkout
Get SwiftPieces Pro

$199 once, every piece included. See how installing works · Sign in

01

What's included

SwiftUI source you own, built on Apple frameworks only.

  • Bag lines with a picture, quantity stepper and confirmed Remove
  • The address with Change, and delivery speeds with their days and prices
  • A promo code that applies, or shows why it was refused
  • Subtotal, delivery, discount, tax and a total that rolls as you change the bag
  • Pay and Apple Pay through to a calm placed-order confirmation

Requires

In-App Purchase capability

02

Implementation notes

How it behaves, what it handles, and the decisions behind it.

The Bag card lists each line with its picture (your image, a remote URL, or an SF Symbol on a neutral square), the name over its variant, and a compact stepper beside the line's price; the price, the item count, the summary and the Pay label roll to their new figures as quantities change, and at one the minus becomes Remove, which asks to confirm. Delivery shows the address with Change, which opens your address picker, then each speed as a selectable row with its day ("Arrives Thu, Oct 23") and price. Payment holds the saved method with Change, a promo field whose Apply checks the code and either adds the discount line or shows why it was refused under the field, and the summary: subtotal, delivery, discount, tax and the total, the block's one large figure. Pay carries the total in the accent and shows a spinner while your closure runs, then a checkmark; a thrown error ("Your card was declined") shows under it and the button is ready to try again. The system Apple Pay button sits below it. Once the order goes through, the block settles into a calm confirmation with the order number, the arrival day, the total paid and View order. Pass nil while the cart loads and the block shows grey shapes in its own layout; an empty bag says so and hides the rest.

Notes

  • CheckoutBlock(cart:onQuantityChange:onPlaceOrder:onApplyPromo:onApplePay:onEditAddress:onChangePayment:onViewOrder:) takes one CheckoutBlock.Cart. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your own column: a ScrollView with .padding(.horizontal, 20), or a sheet.
  • Your app owns the cart and every price. The block only adds them up for display: the subtotal from each line's unitPrice times its quantity, the chosen speed's price, the discount, tax, and the total. Money is Decimal and formats in the person's locale from currencyCode.
  • Cart holds the items, an optional address, the deliveryOptions with the deliveryID chosen at first, an optional payment method, an optional tax and the currencyCode. setQuantity(_:for:) is there for apps that keep the cart as state. Cart.sample is Field Coffee's bag from the preview.
  • An Item has an id, name, optional variant, unitPrice, quantity, maxQuantity (the stepper's upper bound, 10 by default) and a Thumbnail: .image(Image("…")), .url(URL) loaded with AsyncImage, or .symbol("mug"). Lines at quantity zero are left out.
  • DeliveryOption has an id, title, the day it arrives (shown as "Arrives Thu, Oct 23") or your own estimate wording, and a price; zero reads Free. The block keeps the speed chosen in it and hands it to you in the Order.
  • Address is a name, street, optional line2, city, region, postalCode and optional country. PaymentMethod is a title ("Visa •••• 4242"), an optional detail ("Expires 08/28") and an SF Symbol; the block never draws a network logo. Tax is .rate(0.08625), a share of the subtotal after any discount rounded to the currency's minor unit, or .amount(…) that you work out.
  • onQuantityChange gets a line's id and its new quantity. Update your cart and pass it back. Zero arrives after the person confirmed Remove.
  • onPlaceOrder is async throws and gets an Order: the lines, address, speed, payment method, discount and every amount exactly as shown. Return a Confirmation (number, optional arrives and a message line such as where the receipt went) and the block shows the placed order. Throwing shows the error's errorDescription, or "Your payment didn't go through. Try again.", under Pay; throw CancellationError() to go back to rest quietly.
  • onApplyPromo is async throws: check the code and return a Discount with its code, a label ("15% off") and a Value (.amount, .percent(0.15) or .freeDelivery). A thrown error's description shows under the field. Nil hides the field. An applied code shows on the field's row with Remove.
  • onApplePay shows the system PayWithApplePayButton and is async throws like onPlaceOrder. Present the Apple Pay sheet with order.paymentRequest(merchantIdentifier:merchantName:countryCode:), which fills the summary items for you, then place the order and return its confirmation. Nil hides the button.
  • onEditAddress opens your address picker; pass the chosen address back in cart. With no address yet the row reads "No delivery address" with Add. onChangePayment does the same for the payment method. Nil hides Change and Add. onViewOrder gets the Confirmation; nil hides View order.
  • Pay stays disabled with a line under it until there is an address and a payment method. While a payment runs, the bag, delivery and promo code hold still.
  • Haptics: a light tap on Pay, a selection tick for quantities and speeds, success when the order is placed or a code applies, and an error when a payment or code fails.
  • VoiceOver reads each line as its name and variant with "2 for $48.00", the stepper as an adjustable Quantity with a Remove action, each speed as a button with its day and price that is marked selected, the summary lines as label and amount, and the total as one element. Codes applied, failures and "Order placed" are announced.
  • Dynamic Type: at accessibility sizes the pictures step aside so names keep the width, Change moves under the address and the payment method, prices move under each speed's name, the summary amounts and the total stack under their labels, and the stepper grows with the text. Every row keeps its shape down to 320 points.
  • Reduce Motion turns every change into a short fade, and the placeholder holds still instead of breathing.
  • CheckoutBlockExample at the end of CheckoutBlock.swift is the Checkout screen in the preview: Field Coffee's bag, a saved address with Standard and Express, SPRING15 for 15% off (any other code is refused), Change swapping to a second saved address or card, and Pay settling on the placed order. It reads the theme from the environment; pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • Cards are color.surface with radius.lg, 16 point padding and SPHairline dividers in separator, inset to the text; groups sit 24 points apart (space.xl) under type.headline titles.
  • The total is the one display moment: type.numeral(34) in textPrimary, with type.tracking(for:). Rows use type.body, secondary lines and the summary type.subhead with monospaced digits, notes type.footnote; "Order placed" uses the display face at 28 points, capped at semibold, falling back to the text design rather than a monospaced one.
  • Text is textPrimary and textSecondary; row symbols and picture placeholders are textSecondary on fill; empty radio rings and field prompts textTertiary.
  • The stepper and the promo field sit on fill with radius.md; the field adds a 1 point separator stroke that turns textPrimary when focused and danger with a line under it on an error.
  • Pay is the accent with accentOn text in type.headline and radius.md. The chosen speed's check is the only other use of the accent. The Apple Pay button is the system's own, black on light grounds and white on dark ones as Apple asks, at the same 52 point height and corner.
  • The placed check is success; failures are danger.
  • Changes move with motion.smooth, selections and presses with motion.snappy and motion.standard. Every animation becomes a short fade under Reduce Motion.

Requirements

  • For Apple Pay: the Apple Pay capability with a merchant ID. Not needed when you leave onApplePay nil.