TechnologiesSwiftUI

SubscriptionView struct

iOSmacOStvOSwatchOSvisionOSiOS 13.0+✓ renders

A view that subscribes to a publisher with an action.

How it works

SubscriptionView is a view that automatically connects to a Combine Publisher and forwards every value it emits to an action closure. It exists to bridge an external, asynchronous stream of events into your view hierarchy without writing your own Cancellable bookkeeping: the subscription is created when the view appears and torn down when it disappears, so its lifetime is tied to the view that owns it. Reach for it when a piece of UI needs to react to a publisher — a timer, a notification, a model's @Published change — and you want SwiftUI to manage the subscription for you. In practice you rarely instantiate it directly; the onReceive(_:perform:) modifier wraps SubscriptionView so the same machinery hangs off any existing view.

  1. Provide the content with @ViewBuilder content

    The first thing SubscriptionView wraps is the view you actually want on screen, supplied through its @ViewBuilder content parameter and exposed as the content property. The subscription rides alongside this content rather than replacing it — in the example, the membership card built from the crown.fill Image, the Pro Membership Text, and the plan rows would be the content that a SubscriptionView (or the onReceive it backs) attaches to.

  2. Attach the stream through the publisher parameter

    The publisher parameter takes any Combine Publisher whose Failure is Never, and the view subscribes to it on your behalf. This is where you hand over the event source — for the demo screen that could be a publisher tracking entitlement or price changes for the Monthly and Yearly plans, so the UI updates the moment the stream emits.

  3. Respond to each value with the action closure

    Every element the publisher delivers is passed to the action closure, which runs on the main run loop so it is safe to mutate view state from it. Here that closure is the natural home for assigning the result into @State private var selected, exactly as the onTapGesture { selected = plan } handler does — except driven by the publisher instead of a tap.

  4. Let the view manage the subscription lifetime

    Because SubscriptionView conforms to View, it participates in the normal appearance lifecycle: it subscribes when inserted into the hierarchy and cancels when removed, so there is no Cancellable to store. The selected == plan checkmark logic and the Subscribe Button keep working unchanged while the subscription quietly keeps state in sync underneath them.

  5. Prefer onReceive(_:perform:) in everyday code

    Apple exposes this capability primarily through the onReceive(_:perform:) view modifier, which constructs a SubscriptionView for you and layers it onto an existing view. Rather than rebuilding the VStack around an explicit SubscriptionView, you would call .onReceive(planPublisher) { selected = $0 } on the body and get the identical behavior with less ceremony.

Try it — Add .onReceive to the VStack body wired to a Timer.publish(every:1, on:.main, in:.common).autoconnect() publisher whose closure toggles selected between "Monthly" and "Yearly", and watch the checkmark move on its own as the subscription emits.

Example & preview

Press Run live & edit to compile it in your browser — then edit the Swift on the left and the preview re-renders live.

SubscriptionView.swift
struct SubscriptionViewDemo: View {
    @State private var selected = "Yearly"

    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "crown.fill")
                .font(.largeTitle)
                .foregroundStyle(.yellow)
            Text("Pro Membership")
                .font(.title2.bold())
            Text("Unlock all features")
                .foregroundStyle(.secondary)

            ForEach(["Monthly", "Yearly"], id: \.self) { plan in
                HStack {
                    Text(plan)
                    Spacer()
                    Image(systemName: selected == plan ? "checkmark.circle.fill" : "circle")
                }
                .padding()
                .background(selected == plan ? Color.blue.opacity(0.15) : Color.gray.opacity(0.1))
                .clipShape(RoundedRectangle(cornerRadius: 10))
                .onTapGesture { selected = plan }
            }

            Button("Subscribe") {}
                .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}
Live preview
Pro Membership Unlock all features Monthly Yearly Subscribe
Pro Membership Unlock all features Monthly Yearly Subscribe
swift → lexer → parser → sema → uiir → canvas Open in Studio ↗
What's new in SwiftUI 27 →