iOS WebView Integration

This guide explains how to embed the Capture App inside a native iOS application using WKWebView.

Embed the Capture App in a native iOS App

The implementation:

  • Launches the hosted Capture App inside your app
  • Handles camera permissions correctly
  • Grants WebKit-level media access (iOS 15+)
  • Preserves a seamless user experience

How it works

  1. App screen loads.
  2. Camera permission is checked.
  3. If granted:
    • WebView is initialized.
    • Capture URL is loaded.
  4. If denied:
    • User is directed to enable permission in Settings.
  5. Web capture experience runs within the app.

Requirements

Minimum platform

  • Xcode 14+
  • iOS 14+ (iOS 15+ recommended)

Required frameworks

import UIKit
import WebKit
import AVFoundation

Required Info.plist Key

Add the following key:

NSCameraUsageDescription

Example:

Camera access is required to complete the verification process.

Failure to include this key will cause runtime crashes when requesting camera permission.


Step 1 - Request camera permission

Camera permission must be granted before loading the Capture App.

private func requestCameraPermission(completion: @escaping (Bool) -> Void) {
    switch AVCaptureDevice.authorizationStatus(for: .video) {
    case .authorized:
        completion(true)

    case .notDetermined:
        AVCaptureDevice.requestAccess(for: .video) { granted in
            completion(granted)
        }

    case .denied, .restricted:
        completion(false)

    @unknown default:
        completion(false)
    }
}

Behavior

  • If authorized → continues immediately\
  • If first request → iOS permission prompt appears\
  • If denied → must redirect user to Settings

Step 2 - Configure WKWebView

Configure the WebView for mobile rendering and inline media playback.

private func setupWebView() {
    let configuration = WKWebViewConfiguration()

    let preferences = WKWebpagePreferences()
    preferences.preferredContentMode = .mobile
    configuration.defaultWebpagePreferences = preferences
    configuration.allowsInlineMediaPlayback = true

    let webView = WKWebView(frame: .zero, configuration: configuration)
    webView.uiDelegate = self
    self.webView = webView

    view.addSubview(webView)

    webView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        webView.topAnchor.constraint(equalTo: view.topAnchor),
        webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        webView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
    ])
}

Key configuration details

  • preferredContentMode = .mobile ensures mobile layout
  • allowsInlineMediaPlayback = true prevents fullscreen media hijacking
  • Fullscreen constraints provide consistent UX

Step 3 - Load the Capture App URL

private func loadWebPage() {
    guard let webView = self.webView,
          let url = URL(string: "https://CAPTURE_APP_URL") else { return }

    let request = URLRequest(url: url)
    webView.load(request)
}

Requirements

  • Must use HTTPS
  • Replace "https://YOUR_CAPTURE_URL" with the provided Capture App URL
  • Avoid force-unwrapping in production

Step 4 - Handle denied permission

If camera access is denied, direct the user to Settings.

private func showPermissionAlert() {
    let alert = UIAlertController(
        title: "Camera Access Required",
        message: "Please enable camera access in Settings to continue.",
        preferredStyle: .alert
    )

    alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { _ in
        self.openAppSettings()
    })

    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))

    present(alert, animated: true)
}

private func openAppSettings() {
    guard let url = URL(string: UIApplication.openSettingsURLString),
          UIApplication.shared.canOpenURL(url) else { return }

    UIApplication.shared.open(url)
}

Step 5 - Grant WebKit media permission (iOS 15+)

On iOS 15+, WebKit requires explicit handling of media capture permissions.

@available(iOS 15.0, *)
func webView(_ webView: WKWebView,
             decideMediaCapturePermissionsFor origin: WKSecurityOrigin,
             initiatedBy frame: WKFrameInfo,
             type: WKMediaCaptureType) async -> WKPermissionDecision {
    return .grant
}

Recommended security restriction

Restrict permission to trusted domains:

if origin.host == "verify.socure.com" {
    return .grant
}
return .deny

Never auto-grant permissions for unknown origins.



Complete example ViewController

import UIKit
import WebKit
import AVFoundation

class ViewController: UIViewController, WKUIDelegate {

    var webView: WKWebView?

    override func viewDidLoad() {
        super.viewDidLoad()

        requestCameraPermission { [weak self] granted in
            guard let self = self else { return }

            DispatchQueue.main.async {
                if granted {
                    self.setupWebView()
                    self.loadWebPage()
                } else {
                    self.showPermissionAlert()
                }
            }
        }
    }

    private func requestCameraPermission(completion: @escaping (Bool) -> Void) {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            completion(true)
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { granted in
                completion(granted)
            }
        default:
            completion(false)
        }
    }

    private func setupWebView() {
        let config = WKWebViewConfiguration()
        let preferences = WKWebpagePreferences()
        preferences.preferredContentMode = .mobile
        config.defaultWebpagePreferences = preferences
        config.allowsInlineMediaPlayback = true

        let webView = WKWebView(frame: .zero, configuration: config)
        webView.uiDelegate = self
        self.webView = webView
        view.addSubview(webView)

        webView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.topAnchor),
            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }

    private func loadWebPage() {
        guard let webView = webView,
              let url = URL(string: "https://YOUR_CAPTURE_URL") else { return }

        webView.load(URLRequest(url: url))
    }

    private func showPermissionAlert() {
        let alert = UIAlertController(
            title: "Camera Access Required",
            message: "Please enable camera access in Settings to continue.",
            preferredStyle: .alert
        )

        alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { _ in
            self.openAppSettings()
        })

        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        present(alert, animated: true)
    }

    private func openAppSettings() {
        guard let url = URL(string: UIApplication.openSettingsURLString),
              UIApplication.shared.canOpenURL(url) else { return }

        UIApplication.shared.open(url)
    }

    @available(iOS 15.0, *)
    func webView(_ webView: WKWebView,
                 decideMediaCapturePermissionsFor origin: WKSecurityOrigin,
                 initiatedBy frame: WKFrameInfo,
                 type: WKMediaCaptureType) async -> WKPermissionDecision {
        return .grant
    }
}

Best Practices

  • Restrict media permissions to trusted domains
  • Persist evaluation identifiers before launching capture
  • Validate redirect handling server-side
  • Use in-app browsers for a seamless UX