Migrating from ArkoseChallengeView to ArkoseView
This guide is for SwiftUI apps currently using the legacy ArkoseChallengeView (iOS) / ChallengeView (macOS, visionOS) that want to adopt the new native ArkoseView.
This change is additive — ArkoseChallengeView / ChallengeView remain fully supported, so you can migrate at your own pace.
Why migrate
- Cleaner SwiftUI integration — simpler lifecycle and layout.
- One type across platforms —
ArkoseViewreplacesArkoseChallengeView(iOS) andChallengeView(macOS/visionOS). - Unified callbacks — a single
.onChallengeCallbackclosure instead of theArkoseChallengeDelegateprotocol. - You own presentation — inline, or your own
.sheet/.fullScreenCover. - Pre‑warming for lower user‑perceived latency.
Prerequisites
- Arkose Apple SDK v3.0.0 or above.
- Minimum OS: the SDK's package deployment target is unchanged (iOS 12 / macOS 12 / visionOS 1.0).
ArkoseViewitself requires iOS 13 / macOS 12 / visionOS 1.3 at runtime — the same iOS floor asArkoseChallengeView, so you do not need to bump your app's iOS deployment target. Only visionOS's view floor moved (1.0 → 1.3).
Step 1 — Update your dependency to v3.0.0
ArkoseView ships in Apple SDK v3.0.0. This is a major version bump, so package managers pinned to 2.x will not auto‑resolve to it — update your version rule explicitly.
- Swift Package Manager: SPM will not move from a
2.x.xrule to3.0.0on its own. Update the rule to allow 3.0.0 (e.g. "Up to Next Major Version" starting at3.0.0), then resolve packages.
// Package.swift — update the version rule (use the Arkose SPM URL from the Installation guide)
.package(url: "<Arkose SDK SPM URL>", from: "3.0.0")- XCFramework / static library: download the v3.0.0 artifact and replace your embedded framework.
Step 2 — Enable pre‑warming at initialization
Add withPrewarm: true to your existing ArkoseManager.initialize(...) call (recommended for ArkoseView). Call it on the main thread.
// Before
ArkoseManager.initialize(with: config)
// After
ArkoseManager.initialize(with: config, withPrewarm: true)Optional — observe the pre‑warm outcome (iOS 13+). An initialize(...) overload takes a callback that fires once with the pre‑warm result — a single .onReady on success, or a single .onError after retries are exhausted. A later initialize(...) replaces it; the callback‑less overload above is unchanged and keeps the iOS 12 floor.
// Optional (iOS 13+): observe the pre‑warm outcome
ArkoseManager.initialize(with: config, withPrewarm: true) { event in
switch event.type {
case .onReady: break // pre‑warm ready
case .onError: break // pre‑warm failed after retries were exhausted
default: break
}
}Step 3 — Replace the view and its bindings
Swap ArkoseChallengeView for ArkoseView. The presentation binding changes: legacy isPresented both mounted and ran the challenge; with ArkoseView you mount the view yourself (inline or in a modal) and use startEC to run it.
Binding mapping
Legacy (ArkoseChallengeView) | New (ArkoseView) |
|---|---|
isPresented (controls visibility) | Host‑controlled: mount ArkoseView inline, or bind your own .sheet / .fullScreenCover. Use startEC to run. |
runEnforcement (preload‑then‑run trigger) | startEC |
delegate: ArkoseChallengeDelegate | .onChallengeCallback { event in … } |
config: | config: (unchanged) |
withActivity: | withActivity: (unchanged) |
cancelActionConfig: / resetActionConfig: | cancelActionConfig: / resetActionConfig: (unchanged) |
withActivityBackgroundAlpha: | Not available — add your own backdrop when presenting modally (see "Adding a dimmed backdrop" below). |
| (Reset via in‑view button only) | Optional programmatic resetEC binding, plus the Reset button. |
Adding a dimmed backdrop.
ArkoseViewhas no SDK backdrop of its own (this replaceswithActivityBackgroundAlpha). The most reliable way to dim behind the challenge on all iOS versions is aZStackoverlay in your own view — the dim layer and the challenge sit above your (now dimmed) content:ZStack { // your screen content if startEC { Color.black.opacity(0.8).ignoresSafeArea() // dim layer ArkoseView(startEC: $startEC).onChallengeCallback { … } } }If you specifically present via
.sheet/.fullScreenCover, the cover is opaque by default, so a.background(...)on the presenting view is not visible while the cover is up. To dim the app behind a cover, make the cover transparent with.presentationBackground(.clear)(iOS 16.4+) and put the dim layer inside the cover:.fullScreenCover(isPresented: $startEC) { ZStack { Color.black.opacity(0.8).ignoresSafeArea() ArkoseView(startEC: $startEC).onChallengeCallback { … } } .presentationBackground(.clear) // iOS 16.4+ }Verify the backdrop on‑device before shipping.
Before
struct LoginView: View, ArkoseChallengeDelegate {
@State private var isPresented = false
var body: some View {
VStack {
Button("Login") { isPresented = true }
ArkoseChallengeView(isPresented: $isPresented, delegate: self)
}
}
func onCompleted(response: [String: Any?]) { isPresented = false /* use token */ }
func onError(response: [String: Any?]) { isPresented = false }
func onFailed(response: [String: Any?]) { /* check recoverable */ }
}After
struct LoginView: View {
@State private var startEC = false
var body: some View {
VStack {
Button("Login") { startEC = true }
ArkoseView(startEC: $startEC)
.onChallengeCallback { event in
switch event.type {
case .onCompleted: break // use event.response["token"]; the SDK clears startEC for you
case .onFailed:
let recoverable = (event.response["recoverable"] as? Bool) ?? false
_ = recoverable
default: break
}
}
}
}
}Step 4 — Move from ArkoseChallengeDelegate to onChallengeCallback
ArkoseChallengeDelegate to onChallengeCallbackReplace each delegate method with a case in the switch event.type block. The response dictionary is now event.response (same CAPI Response Object — see the "Response payloads" table in the ArkoseView documentation).
Callback mapping
| Legacy delegate method | New event.type | Notes |
|---|---|---|
onReady() | .onReady | |
onShow() | .onShow | |
onShown(response:) | .onShown | payload in event.response |
onSuppress(response:) | .onSuppress | |
onHide(response:) | .onHide | |
onReset() | .onReset | |
onResize(response:) | .onResize | width / height in event.response |
onCompleted(response:) | .onCompleted | token in event.response |
onError(response:) | .onError | |
onWarning(response:) | .onWarning | |
onFailed(response:) | .onFailed | check event.response["recoverable"] |
onPrepareForReset(completion:) | .onPrepareForResetCallback { … } modifier | Dedicated hook, not an onChallengeCallback event — call completion when ready (see below). |
onForceDismissCompleted() | .onForceDismissCompleted |
Reset preparation — before / after
Reset preparation now uses the dedicated onPrepareForResetCallback modifier (not an onChallengeCallback event). With ArkoseView you no longer need to call ArkoseManager.update(with:) here — update the config state you pass to ArkoseView(config:) inside the hook, and the change applies to that same reset. Then call the completion.
// Before (delegate) — update the runtime config explicitly
func onPrepareForReset(completion: @escaping () -> Void) {
ArkoseManager.update(with: newConfig)
completion()
}
// After — use the dedicated onPrepareForResetCallback modifier on ArkoseView
.onPrepareForResetCallback { completion in
// Do any setup needed for this reset — e.g. get a new APISV token or DX blob.
// Then call completion when done (this can be async).
//
// The completion takes an optional ArkoseConfig:
// - pass a config to re-initialize this reset with it (e.g. a language/blob change)
// - pass nil to keep the current configuration.
completion(updatedConfig) // or: completion(nil)
}
- You must call the completion closure or the challenge will not reset.
ArkoseManager.update(with:)/patch(with:)still work and remain a valid explicit alternative (for example, to change only a subset of settings).
Step 5 — Presentation & programmatic dismissal
Presentation is now your app's responsibility.
- Inline: just place
ArkoseViewin your layout. - Modal: wrap it in your own
.sheet/.fullScreenCover. You can bind it to the samestartEC— the SDK setsstartEC = falseon terminal events, which dismisses the modal automatically.
Dismissal mapping
Legacy (ArkoseChallengeView) | New (ArkoseView) |
|---|---|
Set isPresented = false | Set startEC = false — this also dismisses a .sheet / .fullScreenCover bound to startEC. (Or set your own presentation binding to false.) |
.fullScreenCover(isPresented: $startEC) {
ArkoseView(startEC: $startEC)
.onChallengeCallback { event in
switch event.type {
case .onCompleted:
// use event.response["token"]; the SDK sets startEC = false → the cover dismisses
break
default: break
}
}
}Step 6 — Reset & action buttons
ArkoseActionConfig is unchanged. In addition to the in‑SDK Reset button, ArkoseView adds a programmatic resetEC binding:
ArkoseView(
startEC: $startEC,
resetEC: $resetEC, // new: programmatic reset
cancelActionConfig: ArkoseActionConfig(title: "Cancel"),
resetActionConfig: ArkoseActionConfig(title: "Reset")
)Behaviour to note:
resetEConly acts while a challenge is active (startEC == true); toggling it while idle/pre‑warmed is ignored (it will not start a challenge). It is also a one‑shot binding — the SDK setsresetECback tofalseafter handling it (including when ignored) — so you don't need to clear it yourself.
Step 7 — inlineRunOnTrigger
inlineRunOnTriggerYou no longer need to set inlineRunOnTrigger with ArkoseView — preload‑then‑run is its default behaviour. With initialize(withPrewarm: true), the challenge is prepared ahead of time and runs when you set startEC. Drop the flag and the separate runEnforcement binding.
// Before
ArkoseChallengeView(isPresented: $isPresented,
runEnforcement: $runEnforcement,
delegate: self,
withActivity: false,
config: cfg.with(inlineRunOnTrigger: true).build())
// isPresented = true (preload) → runEnforcement = true (run)
// After — no inlineRunOnTrigger flag, no separate runEnforcement binding
ArkoseView(startEC: $startEC,
config: cfg.build(),
withActivity: false)
// view appears (preload) → startEC = true (run)Step 8 — Configuration
No changes. Keep your existing ArkoseConfig.Builder chain — all options (apiBaseUrl, blob, language, userAgent, styleTheme, noSuppress, clientAPIRetryCount, timeoutInSecondsUntilReady, ignoreCookiePersistence, showActivityIndicatorOnReset, challengeBackgroundConfig, …) apply to ArkoseView. (You can drop inlineRunOnTrigger — see Step 7.)
Behavioural differences to verify after migrating
- Presentation is host‑owned —
ArkoseViewnever presents a modal itself; wrap it in your own.sheet/.fullScreenCoverif you need one. - Inline, self‑sizing layout — width is bounded to the available width; height is content‑driven (scrolls in a short/landscape screen rather than being squeezed). Give the host vertical scroll for tall challenges. An explicit
.framesmaller than the challenge clamps and clips. resetECis gated on an active enforcement and auto‑clears (see Step 6).withActivityBackgroundAlphais not available — add your own backdrop for modal presentation (see Step 3).- Deployment target unchanged (iOS) —
ArkoseViewrequires iOS 13 / macOS 12 / visionOS 1.3 at runtime, the same iOS floor asArkoseChallengeView; the SDK package targets are unchanged and only visionOS's view floor moved (1.0 → 1.3). - One type name — replace both
ArkoseChallengeView(iOS) andChallengeView(macOS/visionOS) withArkoseView.
Rollback
Because the change is additive, you can revert a screen to ArkoseChallengeView / ChallengeView at any time without SDK changes.
Updated 1 day ago