iOS In-App Voice SDK
iOS · In-App Voice SDK

Integrate the SecuredCalls Voice SDK

Add branded, verified in-app voice calling to your iOS app. Follow these sections to install the SDK, configure your project, initialize the SDK, place calls, and handle history, missed-call callbacks, and biometric verification.

Estimated time: 40 minutes

iOS 16.0+Xcode 11.0+Swift 5.0+SPM

Overview

The SecuredCalls Voice SDK delivers branded VoIP calls with CallKit integration and push-driven incoming calls. Before you start, register on SecuredCalls.com and obtain your config.dat file and client secret.

Topics

Jump to any part of the integration.

Prerequisites

Make sure your environment is ready.

  • macOS with developer mode enabled
  • Xcode 11.0 or above
  • At least one physical iOS device running iOS 16 or later
  • Swift 5.0 or later
  • A SecuredCalls.com account with your config.dat file and client secret
1

Section 1

Install the SDK

Add SecuredCalls to your project with Swift Package Manager. It takes about two minutes.

STEP 1

Add the package

In Xcode, choose File › Swift Packages › Add Package Dependency…, then paste the repository URL:

https://github.com/expertstack-studios/ios-securevoicecall-sdk

When prompted for a version, select Exact and enter 1.0.25, then click Next and Finish.

2

Section 2

Configure your project

Declare the privacy usage strings the SDK relies on, then enable the required capabilities.

STEP 2

Add privacy keys to Info.plist

The SDK needs microphone, notification, contacts, location, and Face ID access. Add a usage-description string for each so iOS can present the permission prompts.

Tip:

Write user-facing reasons here — App Review rejects vague descriptions.

Info.plistxml
1<key>NSMicrophoneUsageDescription</key>2<string>Explain why microphone access is needed.</string>3 4<key>NSUserNotificationsUsageDescription</key>5<string>Explain why notifications are necessary.</string>6 7<key>NSContactsUsageDescription</key>8<string>Explain why access to contacts is needed.</string>9 10<key>NSLocationWhenInUseUsageDescription</key>11<string>Explain why access to location is needed.</string>12 13<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>14<string>Explain why access to location is needed.</string>15 16<key>NSFaceIDUsageDescription</key>17<string>Explain why biometric authentication is needed.</string>
STEP 3

Enable capabilities

In your target’s Signing & Capabilities tab, click + and add each of the following:

  • Background Modes → check Audio, AirPlay, and Picture in Picture, Voice over IP, Background fetch, and Remote notifications
  • Push Notifications
  • App Groups → configure the identifier group.com.your.app
STEP 4

Enable Keychain Sharing (optional)

Enable this only if you want the SDK’s keychain items to be reachable from both your main app and its notification extension.

  • In Signing & Capabilities, click + and add Keychain Sharing, then add a group, e.g. com.your.app.shared.
  • Repeat for the Notification Service Extension target, using the same group name.
  • Pass that group as sharedKeychainGroup to both SecuredCallsVoice.initialize(...) and SecuredCallsVoice.processNotificationAsync(...).
Warning:

Pass the group without the team-identifier prefix (com.your.app.shared, not ABCDE12345.com.your.app.shared). The SDK resolves and prepends the team ID itself.

Note:

If you omit sharedKeychainGroup, the SDK does not set a keychain access group and its items live in the app’s default group. No extra capability is required.

3

Section 3

Add a Notification Service Extension

This extension lets the SDK modify incoming call pushes before they reach the user.

STEP 5

Create the extension

Add a new Notification Service Extension target, link the SecuredCallsVoiceSDK framework, and add the App Groups capability with the same group.com.your.app identifier.

Then forward the push to the SDK from didReceive.

Important:

The App Group ID here must match the one passed to SecuredCallsVoice.initialize(…) in the main app. If you use Keychain Sharing, sharedKeychainGroup must match too.

NotificationService.swiftswift
1import SecuredCallsVoiceSDK2import UserNotifications3 4class NotificationService: UNNotificationServiceExtension {5    override func didReceive(6        _ request: UNNotificationRequest,7        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void8    ) {9        Task {10            await SecuredCallsVoice.processNotificationAsync(11                request: request,12                // The incoming UNNotificationRequest received by the extension.13 14                appGroupID: "group.com.your.app",15                // MUST be the SAME App Group ID passed to16                // SecuredCallsVoice.initialize(...).17 18                sharedKeychainGroup: "com.your.app.shared",19                // Optional — pass only if you enabled Keychain Sharing.20                // MUST match the value passed to initialize(...), and the group21                // must be enabled on BOTH the app and the extension target.22                // Omit (or pass nil) to use the default keychain group.23 24                withContentHandler: contentHandler25                // Completion handler that returns the modified content to iOS.26            )27        }28    }29 30    override func serviceExtensionTimeWillExpire() {}31}
4

Section 4

Wire up the AppDelegate

SwiftUI apps need an AppDelegate to receive push lifecycle callbacks.

STEP 6

Create an AppDelegate

Create a new Swift file named AppDelegate.swift and define a class conforming to UIApplicationDelegate.

AppDelegate.swiftswift
1import UIKit2 3class AppDelegate: NSObject, UIApplicationDelegate {4    func application(5        _ application: UIApplication,6        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?7    ) -> Bool {8        // Perform any necessary setup here9        return true10    }11}
STEP 7

Connect it to your App

In your @main App struct, use the @UIApplicationDelegateAdaptor property wrapper to attach the delegate.

AppName_SwiftUIApp.swiftswift
1import SwiftUI2 3@main4struct AppName_SwiftUIApp: App {5    // Connect AppDelegate6    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate7 8    var body: some Scene {9        WindowGroup {10            ContentPage()11        }12    }13}
5

Section 5

Initialize the SDK

Boot SecuredCalls inside application(_:didFinishLaunchingWithOptions:).

STEP 8

Customize typography (optional)

Optionally build a Typography object to customize the fonts used across the SDK’s UI surfaces. Every field is optional; omit it entirely to use the SDK’s default system fonts.

Note:

Font customization is purely visual and does not affect SDK behavior. Any nil font falls back to the default.

AppDelegate.swiftswift
1// ALL typography fields are OPTIONAL. If Typography is not provided, or any2// font is nil, the SDK falls back to its default system fonts. Font3// customization is purely visual and does NOT affect SDK behavior.4var typography = Typography(5    displayName: UIFont(name: "AvenirNextLTPro-Demi", size: 36),6    // Large display titles, marquee-style text7 8    timer: UIFont(name: "AvenirNext-Medium", size: 32)?.withMonospacedDigits(),9    // Call duration timer — monospaced digits recommended for stable rendering10 11    callStatus: UIFont(name: "AvenirNext-Regular", size: 20),12    // Call status labels: "Incoming Call", "Connecting", "Dialing"13 14    poweredBy: UIFont(name: "AvenirNext-Medium", size: 18),15    // "Powered by" branding text16 17    callIntentTitle: UIFont(name: "AvenirNext-DemiBold", size: 16),18    // Call intent title text in the in-app call UI19 20    callIntentBody: UIFont(name: "AvenirNext-DemiBold", size: 24),21    // Call intent body / primary message text22 23    buttonTitle: UIFont(name: "AvenirNext-DemiBold", size: 14),24    // Button titles across the SDK UI25 26    callkitCallIntentTitle: UIFont(name: "AvenirNext-Regular", size: 8),27    // Call intent title on the iOS CallKit screen — smaller size recommended28 29    callkitCallIntentBody: UIFont(name: "AvenirNext-DemiBold", size: 16),30    // Call intent body text on the CallKit screen31 32    pipDisplayName: UIFont(name: "AvenirNext-DemiBold", size: 24),33    // Display name shown in Picture-in-Picture (PiP) mode34 35    keypadButtonTitle: UIFont(name: "AvenirNext-DemiBold", size: 24),36    // Dial pad / keypad button text37 38    sheetTitle: UIFont(name: "AvenirNext-DemiBold", size: 20),39    // Title of SDK-presented bottom sheets (e.g. the permission prompt)40 41    sheetBody: UIFont(name: "AvenirNext-Regular", size: 18),42    // Body text of SDK-presented bottom sheets43 44    sheetButtonTitle: UIFont(name: "AvenirNext-DemiBold", size: 18)45    // Button titles inside SDK-presented bottom sheets46)
STEP 9

Call initialize()

Pass your client secret, config file name, settings, and App Group ID. Wrap the call in do/catch to surface configuration errors.

AppDelegate.swiftswift
1func application(2    _ application: UIApplication,3    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?4) -> Bool {5    do {6        try SecuredCallsVoice.initialize(7            "xxxxxxxSECRETxxxxxxx",8            // SecuredCalls client secret provided by the SecuredCalls team.9 10            configFileName: "ConfigFileName",11            // Name of the configuration file (without extension).12 13            settings: ScSDKSettingsModel(14                handlePermission: true,15                // When true, the SDK checks and requests required permissions16                // via system popups if not already granted.17 18                showPipView: true,19                // Enables Picture-in-Picture (PiP) mode during an ongoing call.20 21                logLevel: .debug,22                // .error = 0, .warning = 1, .debug = 2, .information = 3,23                // .off = -1 (Default). Capitalised aliases (.Debug, .Off) work too.24 25                scCallKitIconName: "AppIcon-Mono",26                // Mono-color image name on the CallKit screen. MUST be monochrome.27 28                typography: typography,29                // Optional — if omitted, the SDK uses default typography.30 31                sessionReadyTimeoutSeconds: 3.0,32                // Optional — Default 3.0. Max seconds the SDK waits for its session33                // to become ready before calling back from a missed-call34                // notification. Increase on slow networks.35 36                showDataChannelConnectionStatus: false,37                // Optional — Default false. Shows a connection status indicator on38                // the call screen when the data channel is enabled.39 40                showDataChannelConnectionDetails: false41                // Optional — Default false. Makes the status indicator tappable,42                // expanding into a connection-details panel. No effect unless43                // showDataChannelConnectionStatus is also true.44            ),45 46            appGroupID: "group.com.your.app",47            // The SAME App Group ID must be used in BOTH the main app and the48            // notification extension.49 50            sharedKeychainGroup: "com.your.app.shared",51            // Optional — Default nil. Requires the "Keychain Sharing" capability on52            // the app AND the notification extension, using the same group name.53            // Must match the value passed to processNotificationAsync(...).54 55            biometricMetadata: biometricMetadata56            // Optional — Default nil. See "Biometric verification metadata".57        )58    } catch {59        print("Failed to initialize SecuredCallsVoice SDK: \(error.localizedDescription)")60    }61    return true62}
Declaration
try SecuredCallsVoice.initialize(
  _ secret: String,
  configFileName: String,
  settings: ScSDKSettingsModel,
  appGroupID: String,
  sharedKeychainGroup: String? = nil,
  biometricMetadata: [String: Any]? = nil
) throws
secretString

Your SecuredCalls client secret, provided by the SecuredCalls team.

configFileNameString

Name of the configuration file (without extension) bundled with your app.

settingsScSDKSettingsModel

Runtime options — permission handling, PiP, log level, CallKit icon, optional typography, session-ready timeout, and data-channel status display.

appGroupIDString

App Group identifier used to share data between the main app and the Notification Service Extension.

sharedKeychainGroupString?

Optional. Keychain Sharing group (without the team-ID prefix). Must match the value passed to processNotificationAsync(...). Default nil.

biometricMetadata[String: Any]?

Optional. Application context sent with a successful biometric verification. Default nil.

ScSDKSettingsModel options

Fields on the settings model passed to initialize(...).

handlePermissionBool

When true, the SDK checks and requests required permissions via system popups if not already granted.

showPipViewBool

Enables Picture-in-Picture (PiP) mode so the user can keep using the app during an ongoing call.

logLevelLogLevel

.error = 0, .warning = 1, .debug = 2, .information = 3, .off = -1 (Default). Capitalised aliases (.Debug, .Off, …) still work.

scCallKitIconNameString

Mono-color image name used on the CallKit screen. The image must be a monochrome asset.

typographyTypography?

Optional. Font customization. If omitted, the SDK uses default typography.

sessionReadyTimeoutSecondsDouble

Optional. Default 3.0. Max seconds the SDK waits for its session to become ready before calling back from a missed-call notification. Increase on slow networks.

showDataChannelConnectionStatusBool

Optional. Default false. Shows a connection status indicator on the call screen when the data channel is enabled for the call.

showDataChannelConnectionDetailsBool

Optional. Default false. Makes the status indicator tappable, expanding into a connection-details panel. No effect unless showDataChannelConnectionStatus is also true.

STEP 10

Attach biometric metadata (optional)

biometricMetadata is an optional [String: Any] dictionary of your own application context. When a biometric verification succeeds during a call, the SDK sends this dictionary along with the verification result.

Important:

Supported value types are String, Int, Double, Bool, nil, and arrays or dictionaries of those. Anything else (e.g. Date, Float, a custom type) makes initialize(...) throw SecuredCallError.invalidBiometricMetadata. Passing nil, or omitting the parameter, clears any previously stored metadata.

AppDelegate.swiftswift
1let biometricMetadata: [String: Any] = [2    "CustomerId": userIdentifier,3    "accountId": accountId,4    "verificationContext": "ACCOUNT_ACCESS",5    "location": ["latitude": 37.7749, "longitude": -122.4194],6    "appInfo": [7        "deviceModel": "iPhone 16 Pro",8        "osVersion": "18.3.1",9        "appVersion": "4.2.1"10    ]11]
STEP 11

Request permissions

After initializing, request notification, contact, location, microphone, and biometric access. With handlePermission: true the SDK presents the system prompts for you.

AppDelegate.swiftswift
1UNUserNotificationCenter.current().delegate = self2 3try SecuredCallsVoice.initialize(4    "xxxxxxxSECRETxxxxxxx",5    configFileName: "ConfigFileName",6    settings: ScSDKSettingsModel(7        handlePermission: true,8        showPipView: true,9        logLevel: .Debug,10        scCallKitIconName: "AppIcon-Mono"11    ),12    appGroupID: "group.com.your.app"13)14 15// Request permissions asynchronously16Task {17    await SecuredCallsVoice.requestNotificationPermissionAsync()18    await SecuredCallsVoice.requestContactAccessAsync()19    await SecuredCallsVoice.requestLocationPermissionAsync()20    await SecuredCallsVoice.requestMicrophonePermissionAsync()21    await SecuredCallsVoice.requestBiometricPermissionAsync()22}
6

Section 6

Register, log in, and call

Register the consumer once per install, log the user in, then place calls.

STEP 12

Register the consumer

Call registerConsumerAsync(customerId:) once per app installation, guarding against duplicate registration with UserDefaults.

Note:

For in-app-only calling the identifier can be any string. If you also use PSTN calling, it must be the user’s mobile number.

Registration.swiftswift
1let userIdentifier = "userIdentifier"2let key = "isConsumerRegistered"3 4// Skip if we've already registered on this install5guard !UserDefaults.standard.bool(forKey: key) else {6    print("Already registered")7    return8}9 10do {11    let result = try await SecuredCallsVoice.registerConsumerAsync(customerId: userIdentifier)12    switch result {13    case .success:14        logger.info("success")15    case .failure(let error):16        logger.info("failure: \(error.localizedDescription)")17    }18    UserDefaults.standard.set(true, forKey: key)19} catch {20    print("\(error.localizedDescription)")21}
STEP 13

Log the user in

Register for VoIP pushes, then call loginAsync(identifier:) to establish the session.

AppDelegate.swiftswift
1let userIdentifier = "userIdentifier"2 3func application(4    _ application: UIApplication,5    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil6) -> Bool {7    do {8        UNUserNotificationCenter.current().delegate = self9        registerForVoIPPushes()10        try SecuredCallsVoice.initialize(11            "xxxxxxxSECRETxxxxxxx",12            configFileName: "ConfigFileName",13            settings: ScSDKSettingsModel(14                handlePermission: true,15                showPipView: true,16                logLevel: .Debug,17                scCallKitIconName: "AppIcon-Mono"18            ),19            appGroupID: "group.com.your.app"20        )21 22        Task {23            await SecuredCallsVoice.requestNotificationPermissionAsync()24            await SecuredCallsVoice.requestContactAccessAsync()25            await SecuredCallsVoice.requestLocationPermissionAsync()26            await SecuredCallsVoice.requestMicrophonePermissionAsync()27            await SecuredCallsVoice.requestBiometricPermissionAsync()28 29            let loginResult = await SecuredCallsVoice.loginAsync(identifier: userIdentifier)30            switch loginResult {31            case .success:32                logger.info("success")33            case .failure(let error):34                logger.info("failure: \(error.localizedDescription)")35            }36        }37    } catch {38        print("\(error.localizedDescription)")39    }40    return true41}42 43private func registerForVoIPPushes() {44    let voipRegistry = PKPushRegistry(queue: nil)45    voipRegistry.delegate = self46    voipRegistry.desiredPushTypes = [.voIP]47}
STEP 15

Register the APNs token

Forward the device token from didRegisterForRemoteNotificationsWithDeviceToken to the SDK as a hex string.

AppDelegate.swiftswift
1func application(2    _ application: UIApplication,3    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data4) {5    let token = deviceToken.hexString6    Task {7        // false = sandbox, true = production. Set based on your deployment stage.8        let isProduction = false9        await SecuredCallsVoice.registerDeviceAsync(10            customerId: userIdentifier,11            token: token,12            isProduction: isProduction13        )14    }15}16 17extension Data {18    var hexString: String {19        map { String(format: "%02.2hhx", $0) }.joined()20    }21}
STEP 16

Register the VoIP token

Implement PKPushRegistryDelegate and register the PushKit VoIP token from didUpdate whenever it changes.

AppDelegate+PushKit.swiftswift
1extension AppDelegate: PKPushRegistryDelegate {2    func pushRegistry(3        _ registry: PKPushRegistry,4        didUpdate pushCredentials: PKPushCredentials,5        for type: PKPushType6    ) {7        let isProduction = false8        if type == .voIP {9            Task {10                await SecuredCallsVoice.registerVoipTokenAsync(11                    token: pushCredentials.token,12                    isProduction: isProduction13                )14            }15        }16    }17}
STEP 17

Report incoming VoIP pushes

From didReceiveIncomingPushWith, report the incoming VoIP push to the SDK, then call completion().

AppDelegate+PushKit.swiftswift
1func pushRegistry(2    _ registry: PKPushRegistry,3    didReceiveIncomingPushWith payload: PKPushPayload,4    for type: PKPushType,5    completion: @escaping () -> Void6) {7    if type == .voIP {8        SecuredCallsVoice.reportNewInComingCall(payload: payload)9    }10    completion()11}
STEP 18

Place an outbound call

Start an in-app callback to your contact centre with startCallAsync(...). Only callType is required; the number, intent, and custom data are optional.

CallButton.swiftswift
1Task {2    do {3        try await SecuredCallsVoice.startCallAsync(4            number: "61450000001",5            // Optional — if nil, the contact centre number from your config is used.6 7            callType: .inApp,8            // .inApp places the call through the SDK.9            // .pstn hands off to the native dialer.10 11            callIntent: "Card dispute follow-up",12            // Optional — Default "". Short reason surfaced on the call screen.13 14            customData: ["ticketId": "T-1029"]15            // Optional — Default [:]. Arbitrary key/value context for the call.16        )17    } catch {18        print("Error: \(error)")19    }20}
Declaration
try await SecuredCallsVoice.startCallAsync(
  number: String? = nil,
  callType: ScCallType,
  callIntent: String = "",
  customData: [String: Any] = [:]
) async throws
Important:

startCallAsync throws SecuredCallError.onGoingCall if a call is already active, and SecuredCallError.invalidNumber if no number is available. customData is also accepted by callBackFromCallHistory(...), which derives the call intent itself.

STEP 19

Log the user out

End the session with logoutAsync(identifier:) when the user signs out.

Settings.swiftswift
1Task {2    if let userIdentifier = UserDefaults.standard.string(forKey: "userIdentifier") {3        do {4            try await SecuredCallsVoice.logoutAsync(identifier: userIdentifier)5        } catch {6            print("Logout failed: \(error)")7        }8    }9}
7

Section 7

Handle history & missed-call callbacks

Call back when the user taps a Recents entry, uses Siri, or taps a missed-call notification.

STEP 20

Call back from phone history (SwiftUI)

When a user taps a call entry from the iOS Phone app’s Recents or starts a call with Siri, iOS delivers an NSUserActivity with a system call intent (INStartAudioCallIntent or INStartCallIntent). Extract the call identifier and forward it to callBackFromCallHistory(...).

AppName_SwiftUIApp.swiftswift
1import SwiftUI2import Intents3import SecuredCallsVoiceSDK4 5@main6struct AppName_SwiftUIApp: App {7 8    @UIApplicationDelegateAdaptor(AppDelegate.self)9    var appDelegate10 11    var body: some Scene {12        WindowGroup {13            ContentView()14                .onContinueUserActivity(15                    "INStartAudioCallIntent",16                    perform: handleCallIntent17                )18                .onContinueUserActivity(19                    "INStartCallIntent",20                    perform: handleCallIntent21                )22        }23    }24 25    /// Receives system call intents and forwards the extracted call26    /// identifier to the SecuredCalls Voice SDK.27    private func handleCallIntent(_ userActivity: NSUserActivity) {28        guard let interaction = userActivity.interaction else { return }29 30        var callId: String?31 32        if let intent = interaction.intent as? INStartAudioCallIntent {33            callId = intent.contacts?.first?.personHandle?.value34        } else if let intent = interaction.intent as? INStartCallIntent {35            callId = intent.contacts?.first?.personHandle?.value36        }37 38        guard let callIdentifier = callId else { return }39 40        Task {41            do {42                try await SecuredCallsVoice.callBackFromCallHistory(43                    callId: callIdentifier,44                    callType: .inApp45                )46            } catch {47                print("SecuredCalls: failed to process call intent  \(error.localizedDescription)")48            }49        }50    }51}
STEP 21

Handle missed-call notification taps

When a user misses an incoming call, the SDK schedules a local notification. Add userNotificationCenter(_:didReceive:withCompletionHandler:) to your UNUserNotificationCenterDelegate extension and forward the response to callBackFromMissedCallNotification.

Note:

callBackFromMissedCallNotification only acts on notifications whose categoryIdentifier is MISSED_CALL and returns without doing anything otherwise, so it is safe to forward every response to it.

AppDelegate.swiftswift
1extension AppDelegate: UNUserNotificationCenterDelegate {2 3    func userNotificationCenter(4        _ center: UNUserNotificationCenter,5        didReceive response: UNNotificationResponse,6        withCompletionHandler completionHandler: @escaping () -> Void7    ) {8        Task {9            defer { completionHandler() }10            do {11                try await SecuredCallsVoice.callBackFromMissedCallNotification(response)12            } catch {13                print("callBackFromMissedCallNotification failed: \(error.localizedDescription)")14            }15        }16    }17}
STEP 22

Refresh branding on foreground notifications

If you present notifications while the app is in the foreground, call SecuredCallsVoice.processNotification() so the SDK can notify your SecuredCallsVoiceDelegate that branding history changed.

AppDelegate.swiftswift
1func userNotificationCenter(2    _ center: UNUserNotificationCenter,3    willPresent notification: UNNotification,4    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void5) {6    completionHandler([.banner, .sound])7    SecuredCallsVoice.processNotification()8}

Missed-call notification appearance

What the SDK-scheduled missed-call notification shows.

  • Title: Call intent (if available), otherwise the app/brand name
  • Body: 📞 Missed voice call
  • Action: Tap to call back instantly
8

Section 8

Observe calls, history, and logs

Register delegates to react to call status and history changes, read and clear history, and pull SDK logs.

STEP 23

Observe call status

Conform to ICallStatusDelegate to be notified when a call starts, ends, or fails to connect. Register the delegate once, for example in your view model’s initializer.

Note:

Pass nil to setCallStatusDelegate(_:) to unregister. The delegate is held weakly, so keep a strong reference to the object that conforms to it.

CallViewModel.swiftswift
1import SecuredCallsVoiceSDK2 3final class CallViewModel: ICallStatusDelegate {4 5    init() {6        SecuredCallsVoice.setCallStatusDelegate(self)7    }8 9    func callStarted() {10        // A call is now connected.11    }12 13    func callEnded() {14        // The call finished.15    }16 17    func callFailed(reason: String) {18        // An outgoing call failed before connecting (e.g. poor network).19    }20}
STEP 24

Read call & branding history

The SDK keeps two separate histories: call history (actual voice calls) and branding history (branded caller entries delivered by push). Both methods return [CallInfoModel], exposing callId, intent, note, contactNumber, businessName, brandImage, callType, callTime, isIncomingCall, callDuration, historyType, and branding colours.

HistoryViewModel.swiftswift
1Task {2    switch await SecuredCallsVoice.getCallHistoryAsync() {3    case .success(let calls):4        print("\(calls.count) calls")5    case .failure(let error):6        print("Failed: \(error.localizedDescription)")7    }8 9    switch await SecuredCallsVoice.getBrandingHistoryAsync() {10    case .success(let branding):11        print("\(branding.count) branding entries")12    case .failure(let error):13        print("Failed: \(error.localizedDescription)")14    }15}
STEP 25

Clear history

Clear calls only, branding only, or both.

HistoryViewModel.swiftswift
1Task {2    await SecuredCallsVoice.clearCallHistoryAsync()      // calls only3    await SecuredCallsVoice.clearBrandingHistoryAsync()  // branding only4    await SecuredCallsVoice.clearAllCallHistoryAsync()   // both5}
STEP 26

React to history updates

Conform to SecuredCallsVoiceDelegate to refresh your UI when the SDK writes new history. didUpdateBrandingHistory() has a default empty implementation, so implement only what you need.

Note:

Like the call-status delegate, this delegate is held weakly.

HistoryViewModel.swiftswift
1final class HistoryViewModel: SecuredCallsVoiceDelegate {2 3    init() {4        SecuredCallsVoice.setHistoryDelegate(self)5    }6 7    func didUpdateCallHistory() {8        // Reload call history.9    }10 11    func didUpdateBrandingHistory() {12        // Reload branding history.13    }14}
STEP 27

Read & clear SDK logs

getLogs() returns the SDK’s recorded log lines, and clearLogs() removes them.

Debug.swiftswift
1Task {2    let lines: [String] = await SecuredCallsVoice.getLogs()3    await SecuredCallsVoice.clearLogs()4}

Implementation time

A typical end-to-end integration takes about 40 minutes.

TaskEstimated time
Add the SDK to your project2 min
Configure Info.plist2 min
Enable capabilities2 min
Initialize the SDK2 min
Request permissions2 min
Register & log in5 min
APNs & VoIP token management5 min
Handle notifications & incoming calls5 min
Reporting incoming calls5 min
Callback from phone history5 min
Callback from missed-call notification5 min

Total: ~40 minutes

Note:

Replace "xxxxxxxSECRETxxxxxxx" with your actual client secret throughout. Callback from phone history via AppDelegate and SceneDelegate is documented separately (in progress).