Explore the iOS Checkout SwiftUI architecture interactively. The redesigned SDK is a SwiftUI slot API: a PrimerCheckoutSession owner wired into your view hierarchy with the .primerCheckoutSession(_:onCompletion:) modifier, plus composable views (PrimerCardForm, PrimerPaymentMethods, PrimerVaultedPaymentMethods) whose @ViewBuilder slots default to the *Defaults building blocks. Click any view, slot, or field to see its parameters, lifecycle phases, and copyable Swift code examples.
Two ways to integrate
| Goal | Use |
|---|
| Fastest integration, default screens | PrimerCheckout — the fully managed SwiftUI modal. No slots; it renders Primer’s default screens end to end. |
| Custom SwiftUI layout and slots | PrimerCheckoutSession + .primerCheckoutSession(_:onCompletion:), composing PrimerCardForm / PrimerPaymentMethods / PrimerVaultedPaymentMethods. |
| UIKit apps | PrimerCheckoutPresenter + PrimerCheckoutPresenterDelegate. |
PrimerCheckoutSession is the iOS analog of Android’s PrimerCheckoutHost. Where Android wraps your layout in a composable host, iOS injects the session into the SwiftUI environment with a view modifier — child views read their per-feature session (PrimerCardFormSession, PrimerSelectionSession) from EnvironmentValues.
Slots and defaults
Every composable view exposes @ViewBuilder slots that default to a matching *Defaults namespace builder. Override one slot and the rest keep their default rendering — you never re-implement an entire view.
| View | Slots | Defaults namespace |
|---|
PrimerCardForm | cardDetails, billingAddress, submitButton | CardFormDefaults |
PrimerPaymentMethods | header, method, emptyState | PaymentMethodsDefaults |
PrimerVaultedPaymentMethods | header, item, submitButton | VaultedPaymentMethodsDefaults |
CardFormDefaults also exposes per-field building blocks for recomposing the card section one field at a time: cardNumber, expiryDate, cvv, cardholderName, cardNetwork, plus the billing fields firstName, lastName, email, phoneNumber, addressLine1, addressLine2, city, state, postalCode, countryCode. Each block is self-hiding — it renders nothing unless its field is part of the active CardFormConfiguration. Field presence is Dashboard-driven; visual styling comes from the PrimerCheckoutTheme design tokens.
Sessions and state
Composable views are driven by observable sessions injected into the environment by .primerCheckoutSession(_:onCompletion:). Observe them with @ObservedObject and read their published state.
| Session | Drives | Published state |
|---|
PrimerCheckoutSession | The whole flow (owner) | phase (.initializing / .ready) |
PrimerCardFormSession | PrimerCardForm slots | PrimerCardFormState |
PrimerSelectionSession | PrimerPaymentMethods, PrimerVaultedPaymentMethods | PrimerPaymentMethodSelectionState |
The terminal outcome of the flow is delivered exactly once through the modifier’s onCompletion closure as a PrimerCheckoutState:
| State | Associated values | Description |
|---|
.initializing | — | Loading configuration and payment methods (lifecycle; never delivered to onCompletion). |
.ready | totalAmount: Int, currencyCode: String | Payment methods loaded (lifecycle; never delivered to onCompletion). |
.success | PaymentResult | Payment completed. Carries paymentId, status, and other details. |
.failure | PrimerError | Payment or checkout failed. |
.dismissed | — | Checkout was dismissed without completing a payment. |
| Property | Type | Description |
|---|
configuration | CardFormConfiguration | Which fields the form requires (Dashboard-driven). |
data | FormData | Current field values keyed by PrimerInputElementType. |
fieldErrors | [FieldError] | Validation errors per field. Empty when valid. |
isLoading | Bool | true while a payment is being submitted. |
isValid | Bool | true when all required fields pass validation. |
selectedCountry | PrimerCountry? | Selected billing country, or nil. |
selectedNetwork | PrimerCardNetwork? | Selected network for co-badged cards, or nil. |
availableNetworks | [PrimerCardNetwork] | Detected networks; more than one indicates a co-badged card. |
Composing custom UI
Hold the session as a @StateObject, place the composable views in your layout, and attach the modifier. The child views resolve their per-feature session from the environment — you do not pass a session into them.
import SwiftUI
import PrimerSDK
@available(iOS 15.0, *)
struct CheckoutView: View {
@StateObject private var session: PrimerCheckoutSession
init(clientToken: String) {
_session = StateObject(
wrappedValue: PrimerCheckoutSession(clientToken: clientToken)
)
}
var body: some View {
ScrollView {
VStack(spacing: 24) {
PrimerPaymentMethods()
PrimerCardForm(
cardDetails: { formSession in
VStack(spacing: 16) {
CardFormDefaults.cardNumber(formSession)
HStack(spacing: 16) {
CardFormDefaults.expiryDate(formSession)
CardFormDefaults.cvv(formSession)
}
CardFormDefaults.cardholderName(formSession)
// Co-badged selector: shown only when multiple networks are detected.
CardFormDefaults.cardNetwork(formSession)
}
},
submitButton: { formSession in
Button("Pay now") { formSession.submit() }
.buttonStyle(.borderedProminent)
.disabled(!formSession.state.isValid || formSession.state.isLoading)
}
)
}
.padding()
}
.primerCheckoutSession(session) { state in
handle(state)
}
}
private func handle(_ state: PrimerCheckoutState) {
switch state {
case let .success(result):
print("Payment \(result.paymentId) succeeded")
case let .failure(error):
print("Checkout failed: \(error.localizedDescription)")
case .dismissed:
print("Checkout dismissed")
default:
// .initializing and .ready are lifecycle states and are
// never delivered to onCompletion.
break
}
}
}
Without the .primerCheckoutSession(_:onCompletion:) modifier above them in the hierarchy, composable views such as PrimerCardForm and PrimerPaymentMethods cannot resolve a session and will not function.