=============================== Getting Started with iOS Facade =============================== Acrobits **Facade SDK** contains classes and methods that makes using the native libSoftphone SDK easier. This SDK simplifies the initialization process, configuration process, as well as the handling the push notifications and application lifetime events. This section describes the steps to create an iOS application that can place and receive calls using **Facade SDK**. .. contents:: :local: :depth: 1 ---------------------------- Adding Facade SDK Dependency ---------------------------- To add a package dependency to the Xcode project: .. include:: quick-start.rst :start-after: .. _ios-adding-facade-dependency-start: :end-before: .. _ios-adding-facade-dependency-end: -------------------- Initializing the SDK -------------------- Use the ``SoftphoneFacade.initialize(licenseKey:configuration:)`` method to initialize Facade SDK. The initialization process follows the builder pattern and allows you to configure the SDK using various callbacks that are lazily evaluated when needed, after activating the SDK with the ``SoftphoneFacade/start()`` method. Initialization is fast and can be done even at app launch time due to the ability of lazily evaluation. Therefore, Acrobits recommends developers to initialize the SDK in the ``application(_:didFinishLaunchingWithOptions:)`` method of your application. The following code snippet shows an example of how to initialize Facade SDK with various configurations. Replace certain values for your specific implementation. .. code-block:: groovy :linenos: SoftphoneFacade.instance.initialize(licenseKey: [[ replace with your license key ]], configuration: { (confBuilder : ConfigurationBuilder) -> Void in confBuilder.account(account: {(accountBuilder: AccountBuilder) -> Void in // provide account information let xml = XmlTree(name: "account")! xml.setAttribute(name: "id", value: "acrobits") xml.setNodeValue(value: "1056", name: "username") xml.setNodeValue(value: "secret", name: "password") xml.setNodeValue(value: "host.mypbx.com", name: "host") // there should be only a single account at all times accountBuilder.setSingle(xml: xml) }).preferences(preferences: {(prefsBuilder: PreferencesBuilder) -> Void in // override the defaults for read-only preference keys prefsBuilder.setOverrideDefaultsCallback(callback: {(prefs: SoftphonePreferences) -> Void in prefs.overrideSipisDisabled(false) prefs.overrideDefaultPushNotificationsEnabled(true) prefs.overrideDefaultUserAgentOverride("deliveryHero") prefs.overrideDefaultMaxNumber(ofConcurrentCalls: 1) }) // set values for read-write preference keys prefsBuilder.setModifyPreferencesCallback(callback: {(prefs: SoftphonePreferences) -> Void in prefs.trafficLogging = true }) }).callAssets(callAssets: {(callAssets: CallAssetsBuilder) -> Void in // enhance the contact information with a display name (overriding the display name arriving in the incoming SIP INVITE) and an avatar callAssets.setContactInfoCallback(callback: {(call: SoftphoneCallEvent) -> ContactInfo in let contactInfo = ContactInfo(displayName: "John Doe", avatarUrl: URL(string: "https://images.unsplash.com/photo-1596285508507-5da6bec59433?crop=entropy&cs=tinysrgb&fm=jpg&ixlib=rb-1.2.1&q=80&raw_url=true&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=270")!) return contactInfo }) // return the name of bundle ringtone // default ringtone will be played if // - the callback is not set // - empty string is returned // - the ringtone is not in the app bundle callAssets.setRingtoneCallback { return "relax_ringtone.mp3" } }).localizable { localizableAsset in // return your custom localizable class derived from LocalizableImpl or a custom class implementing Localizable interface localizableAsset.setLocalizableCallback { let appLocalizable = AppLocalizable() return appLocalizable } }.backButtonPolicy { policy in policy.setBackButtonActionCallBack { // possible actions are dismiss and endCall // default is set to endCall return .dismiss } policy.setBackButtonVisibleCallBack { // return true to show back button // return false to hide back button // default is set to true return true } } }) .. note:: * Replace **[[ replace with your license key ]]** with a valid license key obtained in the Acrobits licensing portal. This key is required for initializing ``SoftphoneFacade``. See step 1 in the :ref:`Quick Start Guide `. * To view all the recognized XML nodes and values for SIP account configurations, go to the **Account XML** documentation at `doc.acrobits.net/cloudsoftphone/account.html#account-xml `_. ---------------------------------- Registering for Push Notifications ---------------------------------- After configuring the SDK, proceed to register for push notifications. Once the SDK registers for push notifications, the application can receive both remote notifications, such as from missed calls, and/or PushKit VoIP notifications, which are from incoming calls even when the app is not actively running in the foreground. .. note:: Ensure you already set up the VoIP push notifications in both your project and server prior to registering for push notifications. If the setup is not completed yet, go to the :ref:`iOS Push notifications` section for detailed instructions. There are two options to register for push notifications: * Use the ``SoftphoneFacade/AppDelegateProxy-swift.class/registerForPushNotifications()`` method. This method registers the application to receive both remote notifications and PushKit VOIP notifications. * Register directly from your code. This method allows the developers to decide whether to receive remote notifications or PushKit VOIP notifications. To register for the desired notification type, use the ``setPushToken()`` method of the ``SoftphoneFacade`` to provide the push token obtained from the registration process. The following code snippet gives an example of how to proxy the delegate method to push notifications in the AppDelegate: .. code-block:: groovy :linenos: func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { SoftphoneFacade.instance.appDelegateProxy.didRegisterForRemoteNotificationsWithDeviceToken(deviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { SoftphoneFacade.instance.appDelegateProxy.didFailToRegisterForRemoteNotificationsWithError(error: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { SoftphoneFacade.instance.appDelegateProxy.didReceiveRemoteNotification(userInfo: userInfo, fetchCompletionHandler: completionHandler) } ------------------- Starting Up the SDK ------------------- After initializing the SDK via ``SoftphoneFacade/initialize(licenseKey:configuration:)``, call ``SoftphoneFacade/start()`` to activate Facade SDK. This invokes certain configuration callbacks, depending on the configuration. If a SIP account is provided in ``AccountBuilder``, the SIP account registration process is initiated when starting the SDK. * To check the current status of the SDK, use ``SoftphoneFacade/state-swift.property()``. * To terminate the SDK, use ``SoftphoneFacade/terminate(_:)``. The following code snippet gives an example of how to properly start the SDK: .. code-block:: groovy :linenos: override func viewDidLoad() { super.viewDidLoad() if SoftphoneFacade.state == .terminating { self.callButton.isEnabled = false infoLabel.text = "Waiting..." _ = SoftphoneFacade.instance.addTerminatingCallback({()->Void in self.doStart() }) } else if SoftphoneFacade.state != .running { self.doStart() } else { // started from somewhere else - push handler for instance SoftphoneFacade.instance.addNetworkStateDelegate(self) SoftphoneFacade.instance.addSoftphoneDelegate(self) SoftphoneFacade.instance.addCallRepositoryDelegate(self) guard let state = SoftphoneFacade.instance.getRegistrationState(index: 0) else { return } infoLabel.text = RegistratorState.toString(state) } } func doStart() { SoftphoneFacade.instance.start() SoftphoneFacade.instance.addNetworkStateDelegate(self) SoftphoneFacade.instance.addSoftphoneDelegate(self) SoftphoneFacade.instance.addCallRepositoryDelegate(self) self.callButton.isEnabled = true } deinit { SoftphoneFacade.instance.removeSofthoneDelegate(self) SoftphoneFacade.instance.removeNetworkStateDelegate(self) SoftphoneFacade.instance.removeCallRepositoryDelegate(self) if SoftphoneFacade.state == .running { _ = SoftphoneFacade.instance.terminate(nil) } } ------------------- Reporting Lifecycle ------------------- Facade SDK automatically handles the application's lifecycle management. Hence, developers do not have to take any specific actions regarding the lifecycle management of Facade or the underlying libSoftphone SDK. Once started, the SDK automatically observes lifecycle events of the ``UIScene`` (user interface scene) to detect application transitions between foreground and background states. Based on these observations, the SDK ensures that SIP registration is synchronized with the backend accordingly. ---------------------------------- Terminating and Restarting the SDK ---------------------------------- By using the termination and callback mechanisms, Facade SDK starts and terminates at the appropriate times during the app's runtime. * Use the ``SoftphoneFacade/terminate(_:)`` method to terminate the SDK. * To determine whether the SDK is currently running or in the process of terminating, use the ``SoftphoneFacade/isRunning()`` and ``SoftphoneFacade/isTerminating()`` methods. * If you expect the need to start and terminate the SDK multiple times during app runtime, such as having the SDK available only when specific view controllers are instantiated, ensure to start the SDK only after it is terminated properly. * There are two options to enable notifications when the SDK is completely terminated: * Set a callback function when calling ``SoftphoneFacade/terminate(_:)``. * Use the ``SoftphoneFacade/addTerminatingCallback(_:)`` method to set a callback in the code. ---------------------- Placing Outgoing Calls ---------------------- 1. Prior to making outgoing calls, initialize the SDK by using ``SoftphoneFacade/initialize(licenseKey:configuration:)`` and then start the SDK by using ``SoftphoneFacade/start()`` or ``SoftphoneFacade/ensureStarted()``. 2. To place a call, use the ``SoftphoneFacade/placeCall(_:)`` method. The following snippet code illustrates the process of initiating a call to extension 1078: .. code-block:: :linenos: @IBAction func startCallButtonTapped() { if SoftphoneFacade.instance.getNetwork() == NetworkType_None { // there's no network - we cannot make the call presentNoNetworkScreen() return } if SoftphoneFacade.instance.hasActiveCall() { // the call is already in progress - just show the in call screen self.presentCallScreen() return } if SoftphoneFacade.instance.placeCall("1078") { // show some call-in-progress animation and then move to the in-call screen displayPreCallAnimation() DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.presentCallScreen() } } } private func presentCallScreen() { let callVC = SoftphoneFacade.instance.callViewController if self.navigationController?.presentedViewController != nil { return } callVC.modalTransitionStyle = .crossDissolve callVC.modalPresentationStyle = .fullScreen present(callVC, animated: true) { self.preCallActivityView.isHidden = true } } --------------------------- Handling Push Notifications --------------------------- The push notification registrations option determines whether the application receives incoming push notifications and/or remote notifications, as explained in `Registering for Push Notifications`_. To enable the application to display an incoming call screen, when the application receives an incoming call notification, the SDK triggers appropriate delegate methods, such as the ``call(_:stateChangedTo:)``. * The SDK handles incoming call notifications internally when using ``SoftphoneFacade/AppDelegateProxy-swift.class/registerForPushNotifications()`` to register for push notifications. If you register push notifications directly in the code instead of through the SDK, to ensure the SDK processes the incoming call push notifications or remote notifications: * Incoming call push notifications - Proxy the notifications to the ``SoftphoneFacade/AppDelegateProxy-swift.class/pushRegistry(_:didUpdate:for:)`` method. * Remote notifications - Proxy the notification to their to their respective ``SoftphoneFacade/AppDelegateProxy-swift.class`` methods. * To enable the ``SoftphoneFacade`` to display incoming call screen even if the SDK is not started yet, such as when the SDK is only started in specific view controllers: 1. Implement the ``SoftphoneDelegate`` protocol. 2. Register the ``SoftphoneDelegate`` protocol with ``SoftphoneFacade/addSoftphoneDelegate(_:)``. 3. When you receive a call through ``SoftphoneDelegate/call(_:stateChangedTo:)``, check if the call state is ``CallState_Established`` or ``CallState_IncomingAnswered``. 4. If the call state matches the desired conditions, present the ``SoftphoneFacade/callViewController`` from the current location in your app. The following snippet code demonstrates how the SDK handles push notifications to present the incoming call screen when receiving calls: .. code-block:: :linenos: override func viewDidLoad() { super.viewDidLoad() SoftphoneFacade.instance.addSoftphoneDelegate(self) } deinit { SoftphoneFacade.instance.removeSofthoneDelegate(self) } func call(_ call: SoftphoneCallEvent, stateChangedTo state: CallStateType) { guard state == CallState_Established || state == CallState_IncomingAnswered else { return } let callVC = SoftphoneFacade.instance.callViewController if self.navigationController?.topViewController != self { return } callVC.modalTransitionStyle = .crossDissolve callVC.modalPresentationStyle = .fullScreen present(callVC, animated: true) } ----------------------------- Observing Events from the SDK ----------------------------- To observe events related to account registration and calls in the application, implement the ``SoftphoneDelegate`` protocol and register it with with Facade using ``SoftphoneFacade/addSoftphoneDelegate(_:)``. Here are the event-related methods to implement in the ``SoftphoneDelegate``: .. code-block:: :linenos: // This will be called when the registration state changes. func siphoneStateChanged(_ state: RegistratorStateType) // This will be called when the state of the call changes. (e.g. from Trying to Ringing) func callStateChanged(state: CallStateType, call: SoftphoneCallEvent) // This will be called when the call is put on hold or unheld func holdStatesChanged(states: CallHoldStates, call: SoftphoneCallEvent) // This will be called when there is an incoming call or message func newEvent(_ event: SoftphoneEvent) ---------------- Enabling Logging ---------------- Enable logging to capture important events and activities that occur within the SDK. * By default, the logger is disabled. To enable logging, set the ``trafficLogging`` preference to ``true`` from the ``preferences`` block while initializing the ``SoftphoneFacade``, as shown in the following code snippet: .. code-block:: :linenos: confBuilder.preferences(preferences: {(prefsBuilder: PreferencesBuilder) -> Void in prefsBuilder.setModifyPreferencesCallback(callback: {(prefs: SoftphonePreferences) -> Void in prefs.trafficLogging = true }) }) * To disable logging, set the ``trafficLogging`` preference to ``false`` in the ``preferences`` block. * To direct the SDK logging into the iOS Console, define your own ``NSLogSerializer`` with a custom prefix (````), and then assign it as ``SoftphoneFacade``'s ``customSink`` object, as shown in the following code snippet: .. code-block:: :linenos: let serializer = NSLogSerializer(prefix: "") SoftphoneFacade.instance.customSink = serializer * To retrieve logs at any point, use the following code snippet: .. code-block:: :linenos: let log = SoftphoneFacade.instance.getLogger().get() print(log) ------------------------------- Accessing Telemetry Information ------------------------------- You can access telemetry information through Facade SDK and register to receive specific or all telemetry events. There are two ways to register for telemetry events: * To register for specific events, specify the type using the ``.on(AnyClass, TelemetryEventHandler)`` function. All the events inherit from a common ``TelemetryEvent`` interface. * To listen for all events, use the ``.onAny(TelmetryEventHandler)`` function. The following snippet code demonstrates examples of using these functions to log events: .. code-block:: :linenos: // In order to get all the events we need to use onAny method of telemetry provider SoftphoneFacade.instance.telemetryProvider.onAny { event in print(event.toString()) } // In order to get a specific event (e.g. InAppCallErrorCaused), we need to specify the class SoftphoneFacade.instance.telemetryProvider.on(InAppCallErrorCaused.self) { event in print(event.toString()) } All the events also implement the ``.toString()`` method to convert an event object to its string representation. ------------------------ Customizing Localization ------------------------ Developers can customize the localized strings for the UI elements used in Facade SDK. There are three ways to customize the localization of the SDK: * Create a custom class that implements the ``Localizable`` protocol provided by the SDK. The following snippet code gives an example of the ``muteButtonTitle`` property is overridden with a custom value of ``"Mute"``. .. code-block:: :linenos: class AppLocalizable: Localizable { var muteButtonTitle: String = "Mute" ... } * Derive a custom class from the ``LocalizableImpl`` class, the default implementation of the ``Localizable`` protocol, provided by the SDK. By doing so, you can selectively override specific properties that you want to change while keeping the rest of the localization values intact. The following snippet code gives an example. .. code-block:: :linenos: class AppLocalizable: Localizable { var muteButtonTitle: String = "Mute" ... } * Add an overrides string entry in the ``Localizable.strings`` file of the SDK. The SDK first checks for the translation of the key in the host app bundle. If the key does not exist, the translation in the ``Localizable.strings`` file is used.