Skip to main content

Overview

await Encore.shared.placement(_:).show() returns a PresentationResult: the factual record of the presentation. It never throws, and it resolves exactly once. Three rules cover almost every integration:
  1. Invoke show() from a main-actor context. The facade and builder are @MainActor.
  2. Results are values: await inline for control flow. Branch on the result where the context lives.
  3. outcomes is for passive observers. Analytics, debug overlays, and cross-screen state consume the stream instead of owning a call site.

Rule 1: Invoke show() from a main-actor context

Call show() from a SwiftUI button action, a .task, or a UIKit handler; you are already on the main actor there, and the compiler enforces the rest.
Presentation mutates UI state; invoking it off-main would order the sheet arbitrarily against your progressing app UI. From nonisolated code, await hops you over: it compiles, and the result still resumes on main.

Rule 2: Results are values, await inline for control flow

Branch on the raw facts your flow cares about:
There is no SDK-computed “unlocked” verdict: what a claim means is a property of the flow that served it, so the record reports what happened and your integration decides. When the mechanism matters (analytics, debugging), switch on the factual record:

When the call site can’t await

show(resume:) delivers every outcome, including .notPresented, on the main actor:

Sharing the result across the app

When other parts of the app need the outcome, forward it into an app-owned @Observable state object distributed via .environment(_:):

Rule 3: outcomes is for passive observers

Anything that wants to watch results without owning a call site consumes the multicast outcomes stream. Every resolution lands there, plus late events:
Each access to outcomes returns an independent stream; any number of observers can listen concurrently.

Recipe: paywall-delegate call sites (Superwall and friends)

Delegate methods hand you a synchronous, nonisolated context. Bridge with a Task, forward the result into your state object:
Register your purchase controller once at configure time, so purchases the sheet triggers run through your subscription manager:

Platform-specific notes

  • Task { } wrapping: show() is async; SwiftUI Button actions and UIKit @IBAction selectors are synchronous, so wrap the call in Task { ... }.
  • show() never throws: failures arrive as .notPresented(.error(EncoreError)), read via result.error. There is no catch block to forget.
  • Encore is the entry point: Encore is a public final class. Access it via Encore.shared; the static Encore.placement(_:) forwards to it.
  • Purchases: there are no purchase handlers to register per call site. The EncorePurchaseController passed to configure owns every purchase.

See also