A complete SwiftUI example app demonstrating how to integrate the OmiKit iOS SDK for VoIP calling functionality.
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Remote │ │OMI Server│ │ APNS │ │ OmiKit │ │ CallKit │ │ App │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │ │
│ 1. INVITE │ │ │ │ │
│─────────────>│ │ │ │ │
│ │ 2. VoIP Push│ │ │ │
│ │─────────────>│ │ │ │
│ │ │ 3. Push payload │ │
│ │ │─────────────────────────────> │
│ │ │ │ 4. VoIPPushHandler.handle()│
│ │ │ │<─────────────────────────────
│ │ │ │ 5. Report incoming call │
│ │ │ │─────────────>│ │
│ │ │ │ │ 6. Show CallKit UI
│ │ │ │ │─────────────>│
│ │ │ │ 7. State: incoming (2) │
│ │ │ │─────────────────────────────>
│ │ │ │ │ │
│ │ │ │ User accepts call │
│ │ │ │ │<─────────────│
│ │ │ │ 8. InboundCallAccepted │
│ │ │ │<────────────── │
│ │ │ │ 9. Answer call │
│ │ 10. 200 OK │ │<─────────────────────────────
│<──────────────────────────────────────────│ │ │
│ │ │ │ 11. State: connecting (4) │
│ │ │ │─────────────────────────────>
│ │ │ │ 12. State: confirmed (5) ✅│
│ │ │ │─────────────────────────────>
│ │ │ │ │ 13. Navigate to ActiveCallView
│ │ │ │ │ │ Start timer, Audio ON
│ │ │ │ │ │
│ │ ═══════════ CALL IN PROGRESS ═══════════ │
│ │ │ │ │ │
│ 14. BYE │ │ │ │ │
│─────────────>│ │ │ │ │
│ │ │ │ 15. State: disconnected (6)│
│ │ │ │─────────────────────────────>
│ │ │ │ 16. OMICallDealloc (602) │
│ │ │ │─────────────────────────────>
│ │ │ │ │ 17. Hide call UI
│ │ │ │ │ │ Stop timer
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ App │ │ CallKit │ │ OmiKit │ │OMI Server│ │ Remote │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │
│ 1. User taps "Call" button │ │ │
│ OmiClient.startCall("phone") │ │
│─────────────────────────────> │ │
│ │ 2. Report outgoing call │ │
│ │<─────────────│ │ │
│ │ │ │ │
│ 3. OutboundCallStarted │ │ │
│<────────────── │ │ │
│ │ │ 4. State: calling (1) │
│<───────────────────────────── │ │
│ Show calling UI │ │ │
│ │ │ 5. INVITE │ │
│ │ │─────────────>│ │
│ │ │ │─────────────>│
│ │ │ │ 6. Ringing │
│ │ │ │<─────────────│
│ │ │ 7. State: early (3) │
│<───────────────────────────── │ │
│ Show "Ringing..." │ │ │
│ │ │ │ 8. 200 OK │
│ │ │ │<─────────────│
│ │ │ 9. State: connecting (4) │
│<───────────────────────────── │ │
│ │ │ 10. State: confirmed (5) ✅│
│<───────────────────────────── │ │
│ Start timer, Audio ON │ │ │
│ │ │ │ │
│ │ ═══════════ CALL IN PROGRESS ═══════════│
│ │ │ │ │
│ 11. User ends call │ │ │
│ sipLib.callManager.end() │ │ │
│─────────────────────────────> │ │
│ │ │ 12. BYE │ │
│ │ │─────────────>│ │
│ │ │ │─────────────>│
│ │ │ 13. State: disconnected (6)│
│<───────────────────────────── │ │
│ │ │ 14. OMICallDealloc (601) │
│<───────────────────────────── │ │
│ Hide call UI, Stop timer │ │ │
| Notification | When | Use For |
|---|---|---|
| OMICallStateChanged | Every state transition | Update UI, show/hide call screen, update timer |
| OMICallDealloc | When call ends | Show end reason, trigger missed call notification |
| CallKitProviderDelegateOutboundCallStarted | User starts call via CallKit | Navigate to ActiveCallView |
| CallKitProviderDelegateInboundCallAccepted | User accepts incoming call | Navigate to ActiveCallView, mark as answered |
| OMICallNetworkQuality | Periodic during call | Show network quality indicator (MOS score) |
| State | Code | Description |
|---|---|---|
null |
0 | No call |
calling |
1 | Outgoing call initiated |
incoming |
2 | Incoming call received |
early |
3 | Call ringing |
connecting |
4 | Call connecting |
confirmed |
5 | Call connected ✅ |
disconnected |
6 | Call ended |
hold |
7 | Call on hold |
- SIP authentication (login/logout)
- Outgoing audio/video calls
- Incoming call handling via CallKit
- VoIP push notifications via PushKit
- Call controls (mute, hold, speaker, DTMF)
- Call transfer
- Network quality indicator
- Missed call notifications
- Two implementation patterns: Callback-based (CallManager) and Async/Await (CallManagerV2)
- iOS 13.0+
- Xcode 13.0+ (Xcode 15+ recommended for Swift 6)
- CocoaPods
- OmiKit SDK >= 1.10.31 (latest, with network check support)
- Navigate to the Example directory:
cd Example/SwiftUI-OMICall-Example- Install dependencies:
pod install- Open the workspace:
open SwiftUI-OMICall-Example.xcworkspace- Configure your Apple Developer account and enable:
- Push Notifications capability
- Background Modes: Audio, VoIP, Background fetch, Remote notifications
- App Groups (if needed)
SwiftUI-OMICall-Example/
├── SwiftUI_OMICall_ExampleApp.swift # App entry point & AppDelegate
├── ContentView.swift # Root view with call navigation
├── Core/
│ ├── CallManager.swift # 📌 OmiKit wrapper (CALLBACK-BASED for Swift 5)
│ ├── CallManagerV2.swift # ⚡ OmiKit wrapper (ASYNC/AWAIT for Swift 6)
│ ├── CallKitProviderDelegate.swift # CallKit integration
│ └── PushKitManager.swift # VoIP push handling
├── Views/
│ ├── LoginView.swift # SIP login screen
│ ├── CallingView.swift # Dialpad & call initiation
│ └── ActiveCallView.swift # Active call UI & controls
├── docs/
│
└── Info.plist # App configuration
This example provides two implementations of the CallManager:
Use this if:
- You're using Swift 5 or earlier
- Your project doesn't use async/await
- You prefer traditional completion handlers
- Maximum compatibility with older iOS versions
Pattern:
// Callback-based pattern
CallManager.shared.startCall(to: "123456789") { status in
print("Call status: \(status)")
}
// NotificationCenter observers with @objc selectors
NotificationCenter.default.addObserver(
self,
selector: #selector(handleCallStateChanged),
name: .OMICallStateChanged,
object: nil
)Pros:
- Works on all Swift versions (5.0+)
- Familiar pattern for Objective-C developers
- Well-tested, production-ready
Cons:
- Callback hell for complex flows
- Manual thread management
- More boilerplate code
Use this if:
- You're using Swift 6 Language Mode
- Your project uses modern Swift concurrency (async/await)
- You want cleaner, more maintainable code
- Requires OmiKit >= 1.10.31
Pattern:
// Modern async/await pattern
let status = try await CallManagerV2.shared.startCall(to: "123456789")
print("Call status: \(status)")
// Closure-based observers with queue: .main
callStateObserver = NotificationCenter.default.addObserver(
forName: .OMICallStateChanged,
object: nil,
queue: .main
) { [weak self] notification in
// Extract data BEFORE MainActor context
guard let userInfo = notification.userInfo,
let state = userInfo[OMINotificationUserInfoCallStateKey] as? Int
else { return }
// Now safely update @Published properties
MainActor.assumeIsolated {
self?.callState = state
}
}Pros:
- ✅ Zero Swift 6 concurrency warnings
- ✅ Cleaner code with async/await
- ✅ Better error handling with try/catch
- ✅ Automatic thread safety with
@MainActor - ✅ No data race risks
Cons:
- Requires Swift 6 and OmiKit >= 1.10.31
- Breaking changes if migrating from CallManager
Swift 6 Optimizations in CallManagerV2:
- Uses
@preconcurrency import OmiKitfor smooth interop - All NotificationCenter observers use
queue: .mainto force main queue execution - Extracts all data from
notification.userInfoBEFORE enteringMainActor.assumeIsolatedblock - Posts notifications outside MainActor context to avoid Sendable warnings
- Fully compliant with Swift 6 strict concurrency checking
| Feature | CallManager | CallManagerV2 |
|---|---|---|
| Swift Version | 5.0+ | 6.0+ |
| OmiKit Version Required | Any | Any |
| Pattern | Callbacks | Async/Await |
| Concurrency Warnings | May have warnings on Swift 6 | Zero warnings ✅ |
| Thread Safety | Manual DispatchQueue.main.async |
Automatic @MainActor |
| Error Handling | Completion handlers | try await |
| Code Readability | More verbose | Clean & concise |
| Production Ready | ✅ Yes | ✅ Yes |
If you're upgrading to Swift 6 and want to migrate:
Before (CallManager):
CallManager.shared.login(
username: "user",
password: "pass",
realm: "realm"
) { success in
if success {
print("Logged in")
}
}After (CallManagerV2):
do {
let success = try await CallManagerV2.shared.login(
username: "user",
password: "pass",
realm: "realm"
)
print("Logged in")
} catch {
print("Login failed: \(error)")
}Key Changes:
- Replace
CallManagerwithCallManagerV2 - Add
awaitto all async methods - Wrap in
do-catchfor error handling - Remove completion handler parameters
- Update Podfile to use OmiKit >= 1.10.31
Good news! OmiKit >= 1.10.31 automatically configures Swift 6 compatibility via the podspec. You only need:
1. Update your Podfile:
platform :ios, '13.0'
target 'YourApp' do
use_frameworks!
pod 'OmiKit', '~> 1.10.31' # Latest version with network check
endThat's it! No post_install hook needed - OmiKit.podspec handles all Swift 6 configuration automatically.
2. Set Swift Language Version in Xcode:
- Select your target → Build Settings
- Search for "Swift Language Version"
- Set to "Swift 6"
3. In your code, use @preconcurrency import:
import Foundation
@preconcurrency import OmiKit // Required for Swift 6
@MainActor
class CallManagerV2: ObservableObject {
// Your code here
}What OmiKit.podspec configures automatically:
SWIFT_STRICT_CONCURRENCY = 'minimal'for OmiKit frameworkOTHER_SWIFT_FLAGSwith-Xfrontend -disable-availability-checking- Prevents
dispatch_assert_queue_failcrashes - Allows your app code to use
SWIFT_STRICT_CONCURRENCY = 'complete'
Use CallManagerV2 for modern Swift 6 projects. You only need 2 lines of code in your AppDelegate:
import SwiftUI
import UIKit
import UserNotifications
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Initialize OmiKit SDK with async/await pattern
Task { @MainActor in
await CallManagerV2.shared.initialize(application: application)
}
// Set notification delegate for missed call handling
UNUserNotificationCenter.current().delegate = CallManagerV2.shared
return true
}
func applicationWillTerminate(_ application: UIApplication) {
CallManagerV2.shared.cleanup()
}
}
@main
struct YourApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var callManager = CallManagerV2.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(callManager)
}
}
}Using in SwiftUI Views:
struct LoginView: View {
@EnvironmentObject var callManager: CallManagerV2
var body: some View {
Button("Login") {
Task {
do {
let success = try await callManager.login(
username: "extension",
password: "password",
realm: "realm"
)
print("Login success: \(success)")
} catch {
print("Login error: \(error)")
}
}
}
}
}
struct CallingView: View {
@EnvironmentObject var callManager: CallManagerV2
var body: some View {
Button("Call") {
Task {
do {
let status = try await callManager.startCall(to: "0123456789")
print("Call status: \(status)")
} catch {
print("Call error: \(error)")
}
}
}
}
}The SDK provides a simplified integration through CallManager. You only need 2 lines of code in your AppDelegate:
import SwiftUI
import UIKit
import UserNotifications
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Initialize OmiKit SDK (handles everything: CallKit, PushKit, observers, notifications)
CallManager.shared.initialize(application: application)
// Set notification delegate for missed call handling
UNUserNotificationCenter.current().delegate = CallManager.shared
return true
}
func applicationWillTerminate(_ application: UIApplication) {
// Clean up SDK resources
CallManager.shared.cleanup()
}
}
@main
struct YourApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var callManager = CallManager.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(callManager)
}
}
}- Configures SDK environment (sandbox/production)
- Sets up CallKit provider delegate
- Initializes PushKit for VoIP notifications
- Sets up all notification observers (call state, call end, CallKit events)
- Requests notification permissions
- Handles missed call notifications
- Manages UI navigation state (
shouldShowActiveCallView)
The simplest way to handle call UI navigation - just bind to shouldShowActiveCallView:
struct ContentView: View {
@EnvironmentObject var callManager: CallManager
var body: some View {
LoginView()
.fullScreenCover(isPresented: $callManager.shouldShowActiveCallView) {
ActiveCallView(
phoneNumber: callManager.activeCallPhoneNumber,
isVideo: callManager.activeCallIsVideo,
isPresented: $callManager.shouldShowActiveCallView
)
}
}
}That's it! CallManager automatically:
- Shows
ActiveCallViewwhen incoming call is accepted - Shows
ActiveCallViewwhen outgoing call starts - Hides
ActiveCallViewwhen call ends
struct CallingView: View {
@EnvironmentObject var callManager: CallManager
var body: some View {
VStack {
// Check login status
if callManager.isLoggedIn {
Text("Logged in")
}
// Check call state
if callManager.hasActiveCall {
Text("On call: \(callManager.callState.displayText)")
Text("Duration: \(callManager.formatDuration(callManager.callDuration))")
}
// Make a call
Button("Call") {
callManager.startCall(to: "1234567890") { status in
print("Call status: \(status)")
}
}
}
}
}If you need more control, configure the SDK manually in AppDelegate.didFinishLaunchingWithOptions:
import OmiKit
// Set log level (1-5: Verbose, Debug, Info, Warning, Error)
OmiClient.setLogLevel(2)
// Configure environment
#if DEBUG
OmiClient.setEnviroment(
KEY_OMI_APP_ENVIROMENT_SANDBOX,
userNameKey: "full_name",
maxCall: 1,
callKitImage: "call_image",
typePushVoip: TYPE_PUSH_CALLKIT_DEFAULT
)
#else
OmiClient.setEnviroment(
KEY_OMI_APP_ENVIROMENT_PRODUCTION,
userNameKey: "full_name",
maxCall: 1,
callKitImage: "call_image",
typePushVoip: TYPE_PUSH_CALLKIT_DEFAULT
)
#endifParameters:
environment:KEY_OMI_APP_ENVIROMENT_SANDBOXorKEY_OMI_APP_ENVIROMENT_PRODUCTIONuserNameKey: Key to extract caller name from push payload (e.g., "full_name", "extension")maxCall: Maximum concurrent calls (usually 1)callKitImage: Image name for CallKit UItypePushVoip: Push type (TYPE_PUSH_CALLKIT_DEFAULT,TYPE_PUSH_CALLKIT_CUSTOM, etc.)
// Connect with Sale or Developer for get account testing
OmiClient.initWithUsername(
"extension_number", // e.g., "100"
password: "password", // e.g,. "pKaxGXvzQa8"
realm: "your_realm", // e.g., "omicall"
)
// Configure decline call behavior
OmiClient.configureDeclineCallBehavior(true)OmiClient.logout()OmiClient.startCall("phone_number", isVideo: false) { status in
switch status {
case .startCallSuccess:
print("Call started successfully")
case .invalidUUID:
print("Invalid UUID - cannot find on my page")
case .invalidPhoneNumber:
print("Invalid phone number")
case .samePhoneNumber:
print("Cannot call your own number")
case .maxRetry:
print("Call timeout exceeded, please try again later")
case .permissionDenied:
print("Microphone permission denied")
case .couldNotFindEndpoint:
print("Please login before making your call")
case .accountRegisterFailed:
print("Can't log in to OMI (maybe wrong login information)")
case .startCallFail:
print("Call failed, please try again")
case .haveAnotherCall:
print("Another call in progress")
case .extensionNumberIsOff:
print("Extension number is off - User has turned off")
case .noNetwork:
print("No network connection available")
default:
print("Unknown error")
}
}| Status | Code | Description |
|---|---|---|
INVALID_UUID |
0 | UUID is invalid (cannot find on my page) |
INVALID_PHONE_NUMBER |
1 | SIP user is invalid |
SAME_PHONE_NUMBER_WITH_PHONE_REGISTER |
2 | Cannot call same phone number |
MAX_RETRY |
3 | Call timeout exceeded, please try again later |
PERMISSION_DENIED |
4 | The user has not granted MIC or audio permissions |
COULD_NOT_FIND_END_POINT |
5 | Please login before making your call |
REGISTER_ACCOUNT_FAIL |
6 | Can't log in to OMI (maybe wrong login information) |
START_CALL_FAIL |
7 | Call failed, please try again |
START_CALL_SUCCESS |
8 | Start call successfully |
HAVE_ANOTHER_CALL |
9 | There is another call in progress; please wait for that call to end |
EXTENSION_NUMBER_IS_OFF |
10 | Extension number off - User has turned off |
NO_NETWORK |
11 | No network connection available (WiFi or Cellular) |
let sipLib = OMISIPLib.sharedInstance()
if let call = sipLib.getCurrentCall() {
sipLib.callManager.end(call) { error in
if let error = error {
print("Error ending call: \(error)")
}
}
}let sipLib = OMISIPLib.sharedInstance()
if let call = sipLib.getCurrentCall() {
sipLib.callManager.toggleMute(for: call) { error in
// Handle result
}
}let sipLib = OMISIPLib.sharedInstance()
if let call = sipLib.getCurrentCall() {
sipLib.callManager.toggleHold(for: call) { error in
// Handle result
}
}let sipLib = OMISIPLib.sharedInstance()
let audioController = sipLib.callManager.audioController
// Toggle between speaker and receiver
audioController.output = audioController.output == .speaker ? .other : .speakerif let call = OMISIPLib.sharedInstance().getCurrentCall(),
call.callState == .confirmed {
try? call.sendDTMF("1") // Send digit 1
}if let call = OMISIPLib.sharedInstance().getCurrentCall() {
try? call.blindTransferCall(withNumber: "destination_number")
}Setup CallKit provider delegate:
import OmiKit
class AppDelegate: NSObject, UIApplicationDelegate {
var callKitProviderDelegate: CallKitProviderDelegate?
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
// Setup CallKit
callKitProviderDelegate = CallKitProviderDelegate(
callManager: OMISIPLib.sharedInstance().callManager
)
return true
}
}Setup PushKit for receiving incoming calls when app is in background:
import PushKit
import OmiKit
class PushKitManager: NSObject, PKPushRegistryDelegate {
private var voipRegistry: PKPushRegistry
init(voipRegistry: PKPushRegistry) {
self.voipRegistry = voipRegistry
super.init()
self.voipRegistry.delegate = self
self.voipRegistry.desiredPushTypes = [.voIP]
}
func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType) {
guard type == .voIP else { return }
let token = pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined()
print("VoIP Token: \(token)")
// Send token to OmiKit
OmiClient.setUserPushNotificationToken(token)
}
func pushRegistry(_ registry: PKPushRegistry,
didInvalidatePushTokenFor type: PKPushType) {
guard type == .voIP else { return }
print("VoIP push token invalidated")
}
// CRITICAL: Must report to CallKit immediately or iOS will terminate the app!
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
guard type == .voIP else {
completion()
return
}
print("Received VoIP push: \(payload.dictionaryPayload)")
// IMPORTANT: Use VoIPPushHandler.handle() - NOT OmiClient.receiveIncomingPush()
// The SDK will report to CallKit internally
VoIPPushHandler.handle(payload) {
completion()
}
}
}VoIPPushHandler.handle(payload) when receiving VoIP push.
If you don't report to CallKit immediately, iOS will terminate your app with error:
"Killing app because it never posted an incoming call to the system after receiving a PushKit VoIP push"
Listen for call state notifications:
// In AppDelegate or your view
NotificationCenter.default.addObserver(
self,
selector: #selector(handleCallStateChanged),
name: NSNotification.Name.OMICallStateChanged,
object: nil
)
@objc func handleCallStateChanged(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let stateRaw = userInfo[OMINotificationUserInfoCallStateKey] as? Int else {
return
}
// Call states:
// 0 - null
// 1 - calling (outgoing)
// 2 - incoming
// 3 - early (ringing)
// 4 - connecting
// 5 - confirmed (connected)
// 6 - disconnected
// 7 - hold
// 12 - disconnecting
// Get OMICall object for more info
if let call = userInfo[OMINotificationUserInfoCallKey] as? OMICall {
print("Call ID: \(call.callId)")
print("Is Incoming: \(call.isIncoming)")
print("Is Video: \(call.isVideo)")
print("Caller: \(call.callerNumber ?? "")")
}
}NotificationCenter.default.addObserver(
self,
selector: #selector(handleCallDealloc),
name: NSNotification.Name.OMICallDealloc,
object: nil
)
@objc func handleCallDealloc(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let endCause = userInfo[OMINotificationEndCauseKey] as? Int else {
return
}
// Handle end cause - see Call End Cause Reference table below
print("Call ended with cause: \(endCause)")
}| Code | Description |
|---|---|
| Network & General | |
| 600, 503 | Network operator error or user did not answer the call |
| 408 | Call request timeout (30 seconds waiting time expired) |
| 403 | Service plan only allows calls to dialed numbers. Please upgrade service pack |
| 404 | Current number is not allowed to make calls to the carrier |
| 480 | Number has an error, please contact support |
| Call Rejection | |
| 486 | The listener refuses the call and does not answer |
| 601 | Call ended by the customer |
| 602 | Call ended by the other employee |
| 603 | Call was rejected. Check account limit or call barring configuration |
| Limit Exceeded | |
| 850 | Simultaneous call limit exceeded, please try again later |
| 851 | Call duration limit exceeded, please try again later |
| Account & Service Issues | |
| 852 | Service package not assigned, please contact the provider |
| 853 | Internal number has been disabled |
| 854 | Subscriber is in the DNC (Do Not Call) list |
| 855 | Exceeded allowed number of calls for trial package |
| 856 | Exceeded allowed minutes for trial package |
| 857 | Subscriber has been blocked in the configuration |
| 858 | Unidentified or unconfigured number |
| Carrier Direction Issues | |
| 859 | No available numbers for Viettel direction, please contact the provider |
| 860 | No available numbers for VinaPhone direction, please contact the provider |
| 861 | No available numbers for Mobifone direction, please contact the provider |
| 862 | Temporary block on Viettel direction, please try again |
| 863 | Temporary block on VinaPhone direction, please try again |
| 864 | Temporary block on Mobifone direction, please try again |
| Advertising Restrictions | |
| 865 | Advertising number is outside permitted calling hours, please try again later |
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNetworkQuality),
name: NSNotification.Name.OMICallNetworkQuality,
object: nil
)
@objc func handleNetworkQuality(_ notification: Notification) {
guard let userInfo = notification.userInfo as? [String: Any] else { return }
// MOS (Mean Opinion Score) - 1.0 (poor) to 5.0 (excellent)
if let mos = userInfo[OMINotificationMOSKey] as? Float {
print("Network Quality (MOS): \(mos)")
}
// Additional metrics
if let jitter = userInfo[OMINotificationJitterKey] as? Float {
print("Jitter: \(jitter)ms")
}
if let latency = userInfo[OMINotificationLatencyKey] as? Float {
print("Latency: \(latency)ms")
}
if let packetLoss = userInfo[OMINotificationPPLKey] as? Float {
print("Packet Loss: \(packetLoss)%")
}
}NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioRouteChange),
name: NSNotification.Name.OMICallAudioRouteChange,
object: nil
)
@objc func handleAudioRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo as? [String: Any],
let audioRoute = userInfo["type"] as? String else { return }
// Audio routes: "Speaker", "Receiver", "Bluetooth", "Headphones"
print("Audio route: \(audioRoute)")
}NotificationCenter.default.addObserver(
self,
selector: #selector(handleVideoInfo),
name: NSNotification.Name.OMICallVideoInfo,
object: nil
)
@objc func handleVideoInfo(_ notification: Notification) {
if let userInfo = notification.userInfo as? [String: Any] {
print("Video info: \(userInfo)")
}
}// Track incoming call info
private var lastIncomingCallerNumber: String = ""
private var wasCallAnswered: Bool = false
// In handleCallStateChanged - track incoming call
if stateRaw == 2 && omiCall.isIncoming { // incoming state
lastIncomingCallerNumber = omiCall.callerNumber ?? ""
wasCallAnswered = false
}
if stateRaw == 5 { // confirmed state
wasCallAnswered = true
}
// In handleCallDealloc - show missed call notification
if !wasCallAnswered && !lastIncomingCallerNumber.isEmpty {
showMissedCallNotification(callerNumber: lastIncomingCallerNumber)
}
// Show local notification
func showMissedCallNotification(callerNumber: String, callerName: String, callTime: Date) {
let content = UNMutableNotificationContent()
content.title = "Missed Call"
content.body = "You missed a call from \(callerNumber)"
content.sound = .default
content.badge = NSNumber(value: UIApplication.shared.applicationIconBadgeNumber + 1)
content.userInfo = [
"type": "missed_call",
"omisdkCallerNumber": callerNumber,
"omisdkCallerName": callerName
]
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(
identifier: "missed_call_\(UUID().uuidString)",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request)
}class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
// Show notification in foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound, .badge])
}
// Handle tap on notification
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
if let type = userInfo["type"] as? String, type == "missed_call" {
let callerNumber = userInfo["omisdkCallerNumber"] as? String ?? ""
// Handle missed call tap - e.g., navigate to dial screen or call back
print("User tapped missed call from: \(callerNumber)")
}
// Clear badge
UIApplication.shared.applicationIconBadgeNumber = 0
completionHandler()
}
}// Get available audio input devices
let audioDevices = OmiClient.getAudioInDevices()
// Returns: [[String: String]] with device info
// Set audio output
OmiClient.setAudioOutputs("Speaker") // or "Receiver", "Bluetooth", etc.Handle app termination:
func applicationWillTerminate(_ application: UIApplication) {
OmiClient.omiCloseCall() // Close any active calls
}<!-- Background Modes -->
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>processing</string>
<string>remote-notification</string>
<string>voip</string>
</array>
<!-- Permissions -->
<key>NSMicrophoneUsageDescription</key>
<string>This app requires microphone access to make and receive voice calls.</string>
<key>NSCameraUsageDescription</key>
<string>This app requires camera access to make and receive video calls.</string>| Notification | Description |
|---|---|
OMICallStateChanged |
Call state changed (calling, ringing, connected, etc.) |
OMICallDealloc |
Call ended with reason code |
OMICallNetworkQuality |
Network quality metrics (MOS, jitter, latency) |
OMICallAudioRouteChange |
Audio output changed |
OMICallVideoInfo |
Video call info updated |
CallKitProviderDelegateInboundCallAccepted |
Incoming call accepted via CallKit |
CallKitProviderDelegateOutboundCallStarted |
Outgoing call started via CallKit |
| Value | State | Description |
|---|---|---|
| 0 | null | No call |
| 1 | calling | Outgoing call initiated |
| 2 | incoming | Incoming call received |
| 3 | early | Call ringing |
| 4 | connecting | Call connecting |
| 5 | confirmed | Call connected |
| 6 | disconnected | Call ended |
| 7 | hold | Call on hold |
| 12 | disconnecting | Call ending |
┌──────────┐ ┌────────────┐ ┌───────────┐ ┌──────────────┐
│ incoming │ -> │ connecting │ -> │ confirmed │ -> │ disconnected │
└──────────┘ └────────────┘ └───────────┘ └──────────────┘
│ │
│ (User declines or timeout) │
└─────────────────────────────────────────────────────┘
Flow:
incoming(2) - Incoming call received, CallKit UI displayedconnecting(4) - User accepted, call connectingconfirmed(5) - Call connected, audio establisheddisconnected(6) - Call ended
┌─────────┐ ┌───────┐ ┌────────────┐ ┌───────────┐ ┌──────────────┐
│ calling │ -> │ early │ -> │ connecting │ -> │ confirmed │ -> │ disconnected │
└─────────┘ └───────┘ └────────────┘ └───────────┘ └──────────────┘
│ │
│ (Call failed, busy, no answer, etc.) │
└────────────────────────────────────────────────────────────────┘
Flow:
calling(1) - Outgoing call initiatedearly(3) - Remote party ringingconnecting(4) - Remote party answered, connectingconfirmed(5) - Call connected, audio establisheddisconnected(6) - Call ended
┌───────────┐ ┌──────┐ ┌───────────┐
│ confirmed │ -> │ hold │ -> │ confirmed │
└───────────┘ └──────┘ └───────────┘
Note: During a call, you can toggle between confirmed (5) and hold (7) states
For detailed push notification setup instructions including:
- Creating VoIP Push Certificate in Apple Developer Portal
- Uploading certificate to OMI system
- Xcode project configuration
- Testing push notifications
Please refer to the official guide:
📖 Push Notification Configuration Guide
- Ensure Push Notifications capability is enabled
- Verify VoIP background mode is enabled
- Check that VoIP token is sent to server correctly
- Verify push certificate is valid and matches bundle ID
- Ensure CallKitProviderDelegate is initialized
- Check that PushKit is receiving push correctly
- Verify
VoIPPushHandler.handle(payload)is called in PushKit delegate - Check that completion handler is called after
VoIPPushHandler.handle()
- Request microphone permission before making calls
- Check audio session configuration
- Verify speaker/receiver toggle logic
- Verify SIP credentials (username, password, realm)
- Check network connectivity
- Ensure proxy format is correct:
realm:5222
| Method | Description |
|---|---|
initialize(application:logLevel:) |
Initialize SDK with all components |
cleanup() |
Clean up resources on app termination |
| Method | Pattern | Description |
|---|---|---|
login(username:password:realm:completion:) |
Callback | Login with SIP credentials |
logout() |
Sync | Logout from SIP |
Example:
CallManager.shared.login(
username: "100",
password: "password",
realm: "omicall"
) { success in
print("Login: \(success)")
}| Method | Pattern | Description |
|---|---|---|
startCall(to:isVideo:completion:) |
Callback | Start outgoing call |
endCall(completion:) |
Callback | End current call |
toggleMute(completion:) |
Callback | Toggle mute state |
toggleHold(completion:) |
Callback | Toggle hold state |
toggleSpeaker() |
Sync | Toggle speaker |
sendDTMF(_:) |
Sync | Send DTMF tone |
transferCall(to:) |
Sync | Transfer call |
| Method | Pattern | Description |
|---|---|---|
initialize(application:logLevel:) |
async |
Initialize SDK with all components |
cleanup() |
Sync | Clean up resources on app termination |
Example:
Task { @MainActor in
await CallManagerV2.shared.initialize(application: application)
}| Method | Pattern | Return Type | Description |
|---|---|---|---|
login(username:password:realm:) |
async throws |
Bool |
Login with SIP credentials |
logout() |
async |
Void |
Logout from SIP |
Example:
do {
let success = try await CallManagerV2.shared.login(
username: "100",
password: "password",
realm: "omicall"
)
print("Login success: \(success)")
} catch {
print("Login error: \(error)")
}| Method | Pattern | Return Type | Description |
|---|---|---|---|
startCall(to:isVideo:) |
async throws |
OMIStartCallStatus |
Start outgoing call |
endCall() |
async throws |
Void |
End current call |
toggleMute() |
async throws |
Void |
Toggle mute state |
toggleHold() |
async throws |
Void |
Toggle hold state |
toggleSpeaker() |
Sync | Void |
Toggle speaker |
sendDTMF(_:) |
async throws |
Void |
Send DTMF tone |
transferCall(to:) |
async throws |
Void |
Transfer call |
Example:
// Start call
do {
let status = try await CallManagerV2.shared.startCall(to: "0123456789")
if status == .startCallSuccess {
print("Call started")
}
} catch {
print("Call error: \(error)")
}
// Toggle mute
try await CallManagerV2.shared.toggleMute()
// End call
try await CallManagerV2.shared.endCall()Both CallManager and CallManagerV2 expose the same @Published properties for SwiftUI:
| Property | Type | Description |
|---|---|---|
isLoggedIn |
Bool |
SIP login status |
hasActiveCall |
Bool |
Active call status |
hasIncomingCall |
Bool |
Incoming call flag |
callState |
CallStateStatus / CallStateStatusV2 |
Current call state |
callDuration |
Int |
Call duration in seconds |
isMuted |
Bool |
Mute state |
isSpeakerOn |
Bool |
Speaker state |
isOnHold |
Bool |
Hold state |
currentCall |
OmiCallModel? / OmiCallModelV2? |
Current call info |
incomingCallerNumber |
String |
Incoming caller number |
incomingCallerName |
String |
Incoming caller name |
shouldShowActiveCallView |
Bool |
UI navigation state - bind to fullScreenCover |
Usage in SwiftUI:
struct CallingView: View {
// Use either CallManager or CallManagerV2
@EnvironmentObject var callManager: CallManagerV2
var body: some View {
VStack {
if callManager.isLoggedIn {
Text("Logged In ✅")
}
if callManager.hasActiveCall {
Text("Call State: \(callManager.callState.displayText)")
Text("Duration: \(callManager.formatDuration(callManager.callDuration))")
}
Text("Muted: \(callManager.isMuted ? "Yes" : "No")")
Text("Speaker: \(callManager.isSpeakerOn ? "On" : "Off")")
}
}
}| Property | Type | Description |
|---|---|---|
activeCallPhoneNumber |
String |
Phone number to display for active call |
activeCallIsVideo |
Bool |
Whether active call is video call |
| Method | Description |
|---|---|
formatDuration(_:) |
Format seconds to "MM:SS" |
getAudioOutputs() |
Get available audio devices |
setAudioOutput(_:) |
Set audio output device |
- ✅ You're using Swift 5 or cannot upgrade to Swift 6
- ✅ Your codebase uses traditional completion handlers
- ✅ You need maximum compatibility with older projects
- ✅ You're migrating from Objective-C
- 📁 Reference:
Core/CallManager.swift
- ✅ You're starting a new project with Swift 6
- ✅ You want modern async/await syntax
- ✅ You want zero Swift 6 concurrency warnings
- ✅ You prefer cleaner, more maintainable code
- ✅ You have OmiKit >= 1.10.31
- 📁 Reference:
Core/CallManagerV2.swift
Old Project (Swift 5) New Project (Swift 6)
↓ ↓
CallManager CallManagerV2
(Callbacks) (Async/Await)
↓ ↓
Both work identically for:
- Login/logout
- Make/end calls
- Call controls (mute, hold, speaker, DTMF, transfer)
- @Published properties for SwiftUI
- Missed call notifications
- Network quality monitoring
| Criteria | CallManager | CallManagerV2 |
|---|---|---|
| Swift Version | 5.0+ | 6.0+ |
| Learning Curve | Easy (traditional) | Easy (modern) |
| Concurrency Warnings | May appear on Swift 6 | Zero ✅ |
| Code Verbosity | More verbose | Clean & concise |
| Error Handling | Completion handlers | try/catch |
| Thread Safety | Manual | Automatic |
| Recommendation | Legacy projects | New projects ⭐ |
- Install OmiKit via CocoaPods (
pod install) - Choose implementation: CallManager (Swift 5) or CallManagerV2 (Swift 6)
- If using Swift 6, configure Podfile with
post_installhook - Add
@preconcurrency import OmiKitin Swift 6 projects - Initialize SDK in AppDelegate (
initialize(application:)) - Configure Push Notifications and VoIP capabilities
- Review the Call Flow Diagram
- Test login with SIP credentials
- Test outgoing call
- Test incoming call via VoIP push
- Implement missed call notifications
- Test call controls (mute, hold, speaker, DTMF)
For technical questions or issues:
- 📧 Email: developer@vihatgroup.com
- 📚 Documentation: https://api.omicall.com/web-sdk/mobile-sdk
- 🐛 Report bugs: GitHub Issues
This example is provided as part of the OmiKit SDK. Please refer to the SDK license for usage terms.
Made with ❤️ by VIHAT Team | OmiCall | API Documentation