Apple SDK Integration in UIKit Based Apps

This page covers integrating the Arkose Apple SDK into UIKit based applications using ArkoseManager.showEnforcementChallenge and the ArkoseChallengeDelegate protocol.

If you are building a SwiftUI application, see the main Mobile SDK for Apple page - from v3.0.0, ArkoseView is the recommended SwiftUI integration.

ℹ️

Upgrading from Arkose Apple SDK 2.x to 3.x?

No code changes are required - all existing UIKit integration flows (showEnforcementChallenge / ArkoseChallengeDelegate) are unchanged in v3.x.

You only need to update the SDK dependency to the 3.x version in your project (Swift Package Manager, or xcframework / static library) - see the installation instructions.

Prerequisites

Integration Steps

1. Import the module

Import ArkoseLabsKit or ArkoseLabsKitStatic module before invoking any API from the SDK:

// For dynamic framework
import ArkoseLabsKit

// For static framework
//import ArkoseLabsKitStatic

2. Initialize the SDK

Initialize the SDK as soon as the application launches with an ArkoseConfig object that contains all configuration parameters. We recommend using UIApplicationDelegate didFinishLaunchingWithOptions to do the initialization:

// For dynamic framework
import ArkoseLabsKit

// For static framework
//import ArkoseLabsKitStatic
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        ArkoseManager.initialize(
            with: ArkoseConfig.Builder(withAPIKey: <YOUR_PUBLIC_KEY>)
                    .with(apiBaseUrl: "<Actual API Base URL>")
                    .with(language: "en") //optional
                    .with(clientAPIRetryCount: 0) //optional
                    .with(styleTheme: "") //optional
                    .build()
            )
        return true
    }
}

3. Display the Enforcement Challenge

The SDK offers three ways to display the Enforcement Challenge from UIKit - pick the one that fits your app:

Option A - Modal presentation (recommended)

To run Arkose Bot Manager and display the Enforcement Challenge modally over a UIKit ViewController, invoke ArkoseManager.showEnforcementChallenge from an action method. The SDK presents and dismisses the challenge for you:

ArkoseManager.showEnforcementChallenge(
    parent: self,
    delegate: self
)

See the Appendix: UIKit View Controller below for a complete example.

Option B - App-controlled presentation

If your app needs full control over where and how the challenge appears, use ArkoseManager.createEnforcementChallenge to obtain the SDK's complete challenge screen as a UIViewController and present or embed it yourself. The SDK still manages everything inside the screen (loading indicator, action bar, challenge sizing); your app decides only how the screen is presented:

let challengeViewController = ArkoseManager.createEnforcementChallenge(
    delegate: self
)
present(challengeViewController, animated: true)
// or embed it in your own view hierarchy, e.g. as a child view controller

Because your app owns the presentation, your app is also responsible for dismissing the view controller - handle the terminal delegate callbacks (onCompleted, onError, non-recoverable onFailed, onHide) and dismiss it there.

Option C - Embed the challenge view in your own layout

For the deepest level of control, construct the challenge view itself with ChallengeView(delegate:) and add it to your own view hierarchy. Unlike Option B, there is no SDK-provided screen at all: your app owns the presentation, the dismissal, and the layout of the challenge view.

let challengeView = ChallengeView(delegate: self)
view.addSubview(challengeView)
challengeView.prepareLayout()

Your app is then responsible for:

  • Layout updates. Implement onResize on your delegate and forward the new size to the view:
func onResize(response: [String: Any?]) {
    guard let width = response["width"] as? CGFloat,
          let height = response["height"] as? CGFloat else { return }
    challengeView.updateLayout(height: height, width: width)
}
  • Dismissal. Call challengeView.cancelChallenge() to hide the challenge (this triggers onHide), then remove the view from your hierarchy on terminal callbacks.
  • Reset. Call challengeView.resetChallenge() to reset the current challenge.

4. Receiving Notifications

To receive notifications about various events triggered by the Enforcement Challenge, implement the ArkoseChallengeDelegate protocol on your UIViewController class and pass the instance as the delegate parameter above. To simplify the implementation, ArkoseChallengeDelegate has a default implementation, so implement only the necessary methods for your desired functionality. The most commonly implemented protocol methods are onCompleted, onError, and onFailed to complete the necessary action from the application.

See the ArkoseChallengeDelegate reference for the full list of callbacks and response payloads.

5. Build the revised project

  1. Perform a Clean.
  2. Perform a Build.

6. Run and test the application

  1. Run your modified iOS application.
  2. When running Arkose Bot Manager:
    1. On the integrated screen, confirm you now see an Arkose Enforcement Challenge.
    2. Verify the challenge.
  3. On successful verification, the onCompleted event returns a token as part of the response JSON object.
  4. Send the token to your back-end server for verification.

Update Configuration in SDK

To update the configuration of the SDK any time before the Enforcement Challenge is called, invoke ArkoseManager.update before calling showEnforcementChallenge:

ArkoseManager.update(with:
  ArkoseConfig.Builder(
    withAPIKey: <YOUR_PUBLIC_KEY>)
    .with(language: "fr") // Optional
    .build()
    )

See the Appendix: UIKit View Controller below for a complete example.

Merge instead of replace - ArkoseManager.patch

update(with:) replaces the SDK configuration with the one you pass. To change only specific fields - such as the language or an encrypted data blob - while preserving the rest of the active configuration, use ArkoseManager.patch:

ArkoseManager.patch(with:
  ArkoseConfig.Builder(
    withAPIKey: <YOUR_PUBLIC_KEY>)
    .with(blob: "<NEW_BLOB>") // Optional
    .build()
    )

Fields you do not set on the patched configuration keep their current values. If the SDK has no configuration yet, patch behaves like a first-time setup with the provided configuration.


Preload the Challenge with On-Demand Trigger

The inlineRunOnTrigger configuration lets you preload the Enforcement Challenge to the onReady state, giving your application full control over when a session token is generated or the challenge is presented to the user. This greatly improves user-perceived latency.

  1. Enable preloading via .with(inlineRunOnTrigger: true) in your ArkoseConfig (default: false).
  2. Display the challenge with withActivity: false. The challenge loads to the onReady state without running enforcement, and nothing is shown to the user yet.
  3. When you are ready - for example, when the user taps your Login button - call ArkoseManager.runEnforcement() to resume the enforcement flow.
// 1. Configure with inlineRunOnTrigger
ArkoseManager.update(with:
  ArkoseConfig.Builder(withAPIKey: <YOUR_PUBLIC_KEY>)
    .with(inlineRunOnTrigger: true)
    .build()
    )

// 2. Preload - the challenge loads to onReady without running enforcement
ArkoseManager.showEnforcementChallenge(
    parent: self,
    delegate: self,
    withActivity: false
)

// 3. Later, trigger enforcement on demand
@IBAction func login(_ sender: Any) {
    ArkoseManager.runEnforcement()
}

Notes:

  • Do NOT enable withActivity: true when using this feature, as it will cause the loader to display indefinitely.
  • runEnforcement() has no effect unless the active configuration has inlineRunOnTrigger: true.
  • Calling runEnforcement() when no challenge is loaded is a safe no-op.

Programmatic Dismissal of Enforcement Challenge

Programmatic dismissal allows you to programmatically close an active Enforcement Challenge without user interaction.

This is useful in scenarios such as:

  • Handling app lifecycle events (e.g., app going to background)
  • Implementing timeout mechanisms
  • Responding to external events that require immediate challenge dismissal

Availability

This API is available from SDK version 2.20.0 and above.

Configuration

In UIKit applications, use ArkoseManager.forceDismissEnforcementChallenge() to programmatically dismiss the most recently active Enforcement Challenge.

Method Signature
public static func forceDismissEnforcementChallenge()
Behaviour

This API will dismiss the EC with animation. After successful dismissal, the SDK notifies the application via the onForceDismissCompleted() callback on the delegate.

Handling App Lifecycle Events
class LoginViewController: UIViewController, ArkoseChallengeDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Set up observer for app going to background
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillResignActive),
            name: UIApplication.willResignActiveNotification,
            object: nil
        )
    }

    @objc func appWillResignActive() {
        // Dismiss challenge when app goes to background
        ArkoseManager.forceDismissEnforcementChallenge()
    }

    func onForceDismissCompleted() {
        // Challenge dismissed - app can safely go to background
        print("Challenge dismissed due to app lifecycle event")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    // ... other implementation ...
}
Best Practices

Always implement onForceDismissCompleted()

Even if you don't need to perform specific actions, implementing this callback helps you track dismissal state and handle edge cases.

Wait for dismissal completion before presenting new screens

Do not present new screens or view controllers while programmatic dismissal is in progress. Instead, wait for the onForceDismissCompleted() callback to be triggered before proceeding with navigation or presenting new screens.

The error below indicates that a view controller was presented on top of the Enforcement Challenge during dismissal. If you see this error log, wait for the onForceDismissCompleted() callback before presenting new screens:

Failed to force-dismiss Arkose EC: another app-presented view controller is currently on top of Arkose EC.

API Reference - UIKit

ArkoseManager.showEnforcementChallenge

public static func showEnforcementChallenge(
    parent: UIViewController,
    delegate: ArkoseChallengeDelegate,
    cancelActionConfig: ArkoseActionConfig? = nil,
    resetActionConfig: ArkoseActionConfig? = nil,
    withActivity: Bool? = true,
    withActivityBackgroundAlpha: CGFloat = 0.0
)

Displays the Enforcement Challenge View modally over the parent view controller and invokes the ArkoseChallengeDelegate methods to notify the result.

  • parent: An instance of UIViewController where the Enforcement Challenge View is displayed.
  • delegate: An instance of ArkoseChallengeDelegate to receive event notifications.
  • cancelActionConfig: A structure containing localized String for the title of the Cancel button and localized String for the accessibilityHint of the Cancel button. If set to nil, the Cancel button will not be displayed in the view. Refer to the ArkoseActionConfig section for implementation details.
  • resetActionConfig: A structure containing localized String for the title of the Reset button and localized String for the accessibilityHint of the Reset button. If set to nil, the Reset button will not be displayed in the view. Refer to the ArkoseActionConfig section for implementation details.
  • withActivity: A Bool to control enablement of the loading spinner animation, the default value is true. If this parameter is set to nil, the loading spinner animation is shown in the view.
  • withActivityBackgroundAlpha: A CGFloat value that sets the background alpha of the activity indicator or loader, ranging from 0.0 (fully transparent) to 1.0 (fully opaque), with a default value of 0.0.

ArkoseManager.createEnforcementChallenge

public static func createEnforcementChallenge(
    delegate: ArkoseChallengeDelegate,
    cancelActionConfig: ArkoseActionConfig? = nil,
    resetActionConfig: ArkoseActionConfig? = nil,
    withActivity: Bool? = nil,
    withActivityBackgroundAlpha: CGFloat = 0.0
) -> UIViewController

Creates and returns the SDK's complete challenge screen as a UIViewController without presenting it, so your application decides where and how the screen is presented (modally, or embedded as a child view controller). The SDK manages the contents of the screen; your application is responsible for dismissing the returned view controller on terminal events.

The parameters have the same meaning as in showEnforcementChallenge above.

ChallengeView

public convenience init(
    delegate: ArkoseChallengeDelegate,
    style: UIBlurEffect.Style? = nil
)

ChallengeView is a UIView subclass that renders the Enforcement Challenge itself, for applications that build their own challenge screen. Where createEnforcementChallenge returns a complete, SDK-managed screen, ChallengeView gives you only the challenge view: add it to your own view hierarchy and manage its presentation, layout, and dismissal yourself. Not available in app extensions.

  • delegate: An instance of ArkoseChallengeDelegate to receive event notifications.
  • style: An optional UIBlurEffect.Style for the blurred view background. It takes effect only when challengeBackgroundConfig in the active configuration has blurEffect enabled; otherwise it is ignored. Default nil (no blur background).

Instance methods:

  • public func prepareLayout() : sets up the view's layout constraints. Call it after adding the view to your hierarchy.
  • public func updateLayout(height: CGFloat, width: CGFloat) : resizes the challenge content. Call it from your delegate's onResize callback with the new size.
  • public func resetChallenge() : resets the current challenge.
  • public func cancelChallenge() : cancels and hides the challenge; the delegate receives onHide.

Note: this UIKit ChallengeView is unrelated to the deprecated SwiftUI ChallengeView on macOS/visionOS, which is replaced by ArkoseView.

ArkoseManager.runEnforcement

public static func runEnforcement()

Resumes the enforcement flow of a challenge that was preloaded to the onReady state. Requires inlineRunOnTrigger: true in the active configuration; otherwise the call has no effect. Calling it when no challenge is loaded is a safe no-op. See Preload the Challenge with On-Demand Trigger above.

ArkoseManager.patch

public static func patch(with configuration: ArkoseConfig)

Merges the given configuration into the active SDK configuration: fields set on the provided configuration are applied, and fields you omit keep their current values. Use this to change runtime settings - such as the language or blob - without rebuilding the full configuration. If no configuration exists yet, this behaves like a first-time setup. (Compare update(with:), which replaces the configuration.)

ArkoseManager.forceDismissEnforcementChallenge

public static func forceDismissEnforcementChallenge()

Programmatically dismisses the most recently active Enforcement Challenge with animation. After successful dismissal, the SDK notifies the application via the onForceDismissCompleted() callback on the delegate. If no challenge is active, onForceDismissCompleted() is invoked immediately to indicate there was nothing to dismiss. Available from API version 2.20.0 and above.

Shared types

The following types are shared with the SwiftUI integration and are documented in the Apple SDK Common API Reference:

  • ArkoseConfig.Builder - all configuration parameters (apiBaseUrl, blob, language, userAgent, styleTheme, clientAPIRetryCount, timeoutInSecondsUntilReady, challengeBackgroundConfig, showActivityIndicatorOnReset, and so on)
  • ArkoseConfig
  • ArkoseActionConfig
  • ArkoseChallengeDelegate - all event callbacks (onReady, onShow, onCompleted, onError, onFailed, onPrepareForReset, onForceDismissCompleted, and so on)
  • LogLevel

Appendix: UIKit View Controller

import UIKit
import ArkoseLabsKit

class LoginViewController: UIViewController, ArkoseChallengeDelegate {

    private var isDismissingChallenge = false

    // MARK: - ArkoseChallengeDelegate Methods

    func onCompleted(response: [String: Any?]) {
        print("onComplete received: \(response)")
        // Handle successful challenge completion
    }

    func onError(response: [String: Any?]) {
        print("onError received: \(response)")
        // Handle error during challenge
    }

    func onFailed(response: [String: Any?]) {
        print("onFailed received: \(response)")
        // Handle challenge failure
    }

    func onForceDismissCompleted() {
        isDismissingChallenge = false
        // Now safe to present new screen or perform other actions
        presentNextViewController()
    }

    // MARK: - Actions

    @IBAction func login(_ sender: Any) {
        // Replace <YOUR_PUBLIC_KEY> with the actual API key assigned to your account
        ArkoseManager.update(with: ArkoseConfig.Builder(withAPIKey: "<YOUR_PUBLIC_KEY>")
            .with(language: "fr")
            .build()
        )

        ArkoseManager.showEnforcementChallenge(
            parent: self,
            delegate: self,
            cancelActionConfig: ArkoseActionConfig(
                title: "Cancel",
                accessibilityHint: "Cancel the challenge"
            ),
            resetActionConfig: ArkoseActionConfig(
                title: "Reset",
                accessibilityHint: "Reset the challenge"
            )
        )
    }

    @IBAction func navigateToNextScreen() {
        // ❌ Bad: Presenting immediately after calling dismissal
        // ArkoseManager.forceDismissEnforcementChallenge()
        // presentNextViewController() // May cause conflicts

        // ✅ Good: Wait for dismissal completion
        if isDismissingChallenge {
            // Already dismissing, wait for callback
            return
        }

        isDismissingChallenge = true
        ArkoseManager.forceDismissEnforcementChallenge()
        // presentNextViewController() will be called in onForceDismissCompleted()
    }

    @IBAction func dismiss(_ sender: Any) {
        if isDismissingChallenge {
            // Already dismissing, wait for callback
            return
        }

        isDismissingChallenge = true
        ArkoseManager.forceDismissEnforcementChallenge()
        // Dismissal completion will be notified via onForceDismissCompleted()
    }

    // MARK: - Helper Methods

    private func presentNextViewController() {
        let nextVC = NextViewController()
        present(nextVC, animated: true)
    }
}