AzureCommunicationUI/AzureCommunicationUIDemoApp/Sources/Views/CallingDemoViewController.swift (1,267 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import UIKit import Combine import SwiftUI import AzureCommunicationCommon import AppCenterCrashes import AVFoundation import CallKit #if DEBUG @testable import AzureCommunicationUICalling #else import AzureCommunicationUICalling #endif class CallingDemoViewController: UIViewController { enum LayoutConstants { static let verticalSpacing: CGFloat = 8.0 static let stackViewSpacingPortrait: CGFloat = 18.0 static let stackViewSpacingLandscape: CGFloat = 12.0 static let buttonHorizontalInset: CGFloat = 20.0 static let buttonVerticalInset: CGFloat = 10.0 } var callingViewModel: CallingDemoViewModel private var selectedAcsTokenType: ACSTokenType = .token private var acsTokenUrlTextField: UITextField! private var acsTokenTextField: UITextField! private var selectedMeetingType: MeetingType = .groupCall private var displayNameTextField: UITextField! private var userIdTextField: UITextField! private var groupCallTextField: UITextField! private var teamsMeetingTextField: UITextField! private var teamsMeetingIdTextField: UITextField! private var teamsMeetingPasscodeTextField: UITextField! private var participantMRIsTextField: UITextField! private var roomCallTextField: UITextField! private var settingsButton: UIButton! private var showCallHistoryButton: UIButton! private var registerPushButton: UIButton! private var unregisterPushButton: UIButton! private var acceptCallButton: UIButton! private var declineCallButton: UIButton! private var startExperienceButton: UIButton! private var showExperienceButton: UIButton! private var acsTokenTypeSegmentedControl: UISegmentedControl! private var meetingTypeSegmentedControl: UISegmentedControl! private var stackView: UIStackView! private var titleLabel: UILabel! private var callStateLabel: UILabel! private var titleLabelConstraint: NSLayoutConstraint! private var callStateLabelConstraint: NSLayoutConstraint! private var incomingCallId = "" private var isIncomingCall = false // The space needed to fill the top part of the stack view, // in order to make the stackview content centered private var spaceToFullInStackView: CGFloat? private var userIsEditing = false private var isKeyboardShowing = false private var exitCompositeExecuted = false private var cancellable = Set<AnyCancellable>() private var envConfigSubject: EnvConfigSubject #if DEBUG private var callingSDKWrapperMock: UITestCallingSDKWrapper? #endif private lazy var contentView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var scrollView: UIScrollView = { let view = UIScrollView() view.translatesAutoresizingMaskIntoConstraints = false view.showsVerticalScrollIndicator = false view.showsHorizontalScrollIndicator = false return view }() override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateUIBasedOnUserInterfaceStyle() if UIDevice.current.orientation.isPortrait { stackView.spacing = LayoutConstants.stackViewSpacingPortrait titleLabelConstraint.constant = 32 } else if UIDevice.current.orientation.isLandscape { stackView.spacing = LayoutConstants.stackViewSpacingLandscape titleLabelConstraint.constant = 16.0 } } #if DEBUG init(envConfigSubject: EnvConfigSubject, callingViewModel: CallingDemoViewModel, callingSDKHandlerMock: UITestCallingSDKWrapper? = nil) { self.envConfigSubject = envConfigSubject self.callingViewModel = callingViewModel self.callingSDKWrapperMock = callingSDKHandlerMock super.init(nibName: nil, bundle: nil) self.combineEnvConfigSubject() } #else init(envConfigSubject: EnvConfigSubject, callingViewModel: CallingDemoViewModel) { self.envConfigSubject = envConfigSubject self.callingViewModel = callingViewModel super.init(nibName: nil, bundle: nil) self.combineEnvConfigSubject() } #endif required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupUI() registerNotifications() } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() guard !userIsEditing else { return } scrollView.setNeedsLayout() scrollView.layoutIfNeeded() let emptySpace = stackView.customSpacing(after: stackView.arrangedSubviews.first!) let spaceToFill = (scrollView.frame.height - (stackView.frame.height - emptySpace)) / 2 stackView.setCustomSpacing(spaceToFill + LayoutConstants.verticalSpacing, after: stackView.arrangedSubviews.first!) } private func combineEnvConfigSubject() { envConfigSubject.objectWillChange .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true).sink(receiveValue: { [weak self] _ in self?.updateFromEnvConfig() }).store(in: &cancellable) } private func updateFromEnvConfig() { if envConfigSubject.useExpiredToken { updateToken(envConfigSubject.expiredAcsToken) } else { updateToken(envConfigSubject.acsToken) } if !envConfigSubject.displayName.isEmpty { displayNameTextField.text = envConfigSubject.displayName } if !envConfigSubject.userId.isEmpty { userIdTextField.text = envConfigSubject.userId } if !envConfigSubject.groupCallId.isEmpty { groupCallTextField.text = envConfigSubject.groupCallId } if !envConfigSubject.teamsMeetingLink.isEmpty { teamsMeetingTextField.text = envConfigSubject.teamsMeetingLink } if !envConfigSubject.teamsMeetingId.isEmpty { teamsMeetingIdTextField.text = envConfigSubject.teamsMeetingId } if !envConfigSubject.teamsMeetingPasscode.isEmpty { teamsMeetingPasscodeTextField.text = envConfigSubject.teamsMeetingPasscode } if !envConfigSubject.participantMRIs.isEmpty { participantMRIsTextField.text = envConfigSubject.participantMRIs } if envConfigSubject.selectedMeetingType == .groupCall { meetingTypeSegmentedControl.selectedSegmentIndex = 0 } else if envConfigSubject.selectedMeetingType == .teamsMeeting { meetingTypeSegmentedControl.selectedSegmentIndex = 1 } if !envConfigSubject.roomId.isEmpty { roomCallTextField.text = envConfigSubject.roomId } } private func updateToken(_ token: String) { if !token.isEmpty { acsTokenTextField.text = token acsTokenTypeSegmentedControl.selectedSegmentIndex = 1 } } private func onError(_ error: CallCompositeError, callComposite: CallComposite) { print("::::UIKitDemoView::getEventsHandler::onError \(error)") print("::::UIKitDemoView error.code \(error.code)") callingViewModel.callHistory.last?.callIds.forEach { print("::::UIKitDemoView call id \($0)") } } private func onRemoteParticipantJoined(to callComposite: CallComposite, identifiers: [CommunicationIdentifier]) { print("::::UIKitDemoView::getEventsHandler::onRemoteParticipantJoined \(identifiers)") guard envConfigSubject.useCustomRemoteParticipantViewData else { return } RemoteParticipantAvatarHelper.onRemoteParticipantJoined(to: callComposite, identifiers: identifiers) } private func onCallStateChanged(_ callState: CallState, callComposite: CallComposite) { print("::::CallingDemoViewController::getEventsHandler::onCallStateChanged \(callState.requestString)") callStateLabel.text = callState.requestString } func createCallComposite() async -> CallComposite? { print("CallingDemoView:::: createCallComposite requesting") if GlobalCompositeManager.callComposite != nil { print("CallingDemoView:::: createCallComposite exist") return GlobalCompositeManager.callComposite! } print("CallingDemoView:::: createCallComposite creating") var localizationConfig: LocalizationOptions? let layoutDirection: LayoutDirection = envConfigSubject.isRightToLeft ? .rightToLeft : .leftToRight let barOptions = CallScreenControlBarOptions(leaveCallConfirmationMode: envConfigSubject.displayLeaveCallConfirmation ? .alwaysEnabled : .alwaysDisabled) if !envConfigSubject.localeIdentifier.isEmpty { let locale = Locale(identifier: envConfigSubject.localeIdentifier) localizationConfig = LocalizationOptions(locale: locale, layoutDirection: layoutDirection) } else if !envConfigSubject.locale.identifier.isEmpty { localizationConfig = LocalizationOptions( locale: envConfigSubject.locale, layoutDirection: layoutDirection) } var callScreenOptions = CallScreenOptions(controlBarOptions: barOptions , headerViewData: CallScreenHeaderViewData( title: "This is a custom header", subtitle: "This is a custom subtitle") ) let setupViewOrientation = envConfigSubject.setupViewOrientation let setupScreenOptions = SetupScreenOptions( cameraButtonEnabled: envConfigSubject.setupScreenOptionsCameraButtonEnabled, microphoneButtonEnabled: envConfigSubject.setupScreenOptionsMicButtonEnabled) let callingViewOrientation = envConfigSubject.callingViewOrientation let callKitOptions = envConfigSubject.enableCallKit ? getCallKitOptions() : nil let userId = CommunicationUserIdentifier(envConfigSubject.userId) let callCompositeOptions = envConfigSubject.useDeprecatedLaunch ? CallCompositeOptions( theme: envConfigSubject.useCustomColors ? CustomColorTheming(envConfigSubject: envConfigSubject) : Theming(envConfigSubject: envConfigSubject), localization: localizationConfig, setupScreenOrientation: setupViewOrientation, callingScreenOrientation: callingViewOrientation, enableMultitasking: envConfigSubject.enableMultitasking, enableSystemPictureInPictureWhenMultitasking: envConfigSubject.enablePipWhenMultitasking, callScreenOptions: callScreenOptions, callKitOptions: callKitOptions, setupScreenOptions: setupScreenOptions) : CallCompositeOptions( theme: envConfigSubject.useCustomColors ? CustomColorTheming(envConfigSubject: envConfigSubject) : Theming(envConfigSubject: envConfigSubject), localization: localizationConfig, setupScreenOrientation: setupViewOrientation, callingScreenOrientation: callingViewOrientation, enableMultitasking: envConfigSubject.enableMultitasking, enableSystemPictureInPictureWhenMultitasking: envConfigSubject.enablePipWhenMultitasking, callScreenOptions: callScreenOptions, callKitOptions: callKitOptions, displayName: envConfigSubject.displayName, disableInternalPushForIncomingCall: envConfigSubject.disableInternalPushForIncomingCall, setupScreenOptions: setupScreenOptions) let useMockCallingSDKHandler = envConfigSubject.useMockCallingSDKHandler if let credential = try? await getTokenCredential() { #if DEBUG let callComposite = useMockCallingSDKHandler ? CallComposite(withOptions: callCompositeOptions, callingSDKWrapperProtocol: callingSDKWrapperMock) : ( envConfigSubject.useDeprecatedLaunch ? CallComposite(withOptions: callCompositeOptions) : CallComposite(credential: credential, withOptions: callCompositeOptions)) callingSDKWrapperMock?.callComposite = callComposite #else let callComposite = envConfigSubject.useDeprecatedLaunch ? CallComposite(withOptions: callCompositeOptions) : CallComposite(credential: credential, withOptions: callCompositeOptions) #endif subscribeToEvents(callComposite: callComposite) GlobalCompositeManager.callComposite = callComposite self.envConfigSubject.saveFromState() return callComposite } return nil } func apiDemo() { let credential = (try? CommunicationTokenCredential(token: acsTokenTextField.text!))! let callComposite = CallComposite(credential: credential) let customButton = CustomButtonViewData(id: UUID().uuidString, image: UIImage(), title: "Hide composite") {_ in // hide call composite and display Troubleshooting tips callComposite.isHidden = true // ... } let cameraButton = ButtonViewData(visible: false) let micButton = ButtonViewData(enabled: false) let callScreenControlBarOptions = CallScreenControlBarOptions( cameraButton: cameraButton, microphoneButton: micButton, customButtons: [customButton] ) let callScreenOptions = CallScreenOptions(controlBarOptions: callScreenControlBarOptions) let localOptions = LocalOptions(callScreenOptions: callScreenOptions) callComposite.launch(locator: .roomCall(roomId: "..."), localOptions: localOptions) } func subscribeToEvents(callComposite: CallComposite) { let onRemoteParticipantJoinedHandler: ([CommunicationIdentifier]) -> Void = { [weak callComposite] ids in guard let composite = callComposite else { return } self.onRemoteParticipantJoined(to: composite, identifiers: ids) } let onErrorHandler: (CallCompositeError) -> Void = { [weak callComposite] error in guard let composite = callComposite else { return } self.onError(error, callComposite: composite) } let onPipChangedHandler: (Bool) -> Void = { isPictureInPicture in print("::::CallingDemoView:onPipChangedHandler: ", isPictureInPicture) } let onUserReportedIssueHandler: (CallCompositeUserReportedIssue) -> Void = { issue in print("::::UIKitDemoView::getEventsHandler::onUserReportedIssue \(issue)") } let onCallStateChangedHandler: (CallState) -> Void = { [weak callComposite] callStateEvent in guard let composite = callComposite else { return } self.onCallStateChanged(callStateEvent, callComposite: composite) } let onDismissedHandler: (CallCompositeDismissed) -> Void = { [] _ in if self.envConfigSubject.useRelaunchOnDismissedToggle && self.exitCompositeExecuted { DispatchQueue.main.async { Task { @MainActor in self.onStartExperienceBtnPressed() } } } } exitCompositeExecuted = false if !envConfigSubject.exitCompositeAfterDuration.isEmpty { DispatchQueue.main.asyncAfter(deadline: .now() + Float64(envConfigSubject.exitCompositeAfterDuration)! ) { [weak callComposite] in self.exitCompositeExecuted = true callComposite?.dismiss() } } let callKitCallAccepted: (String) -> Void = { [weak callComposite] callId in self.acceptCallButton.isHidden = true self.declineCallButton.isHidden = true guard let callComposite = callComposite else { return } callComposite.launch(callIdAcceptedFromCallKit: callId, localOptions: self.getLocalOptions(callComposite)) } let onIncomingCall: (IncomingCall) -> Void = { [] incomingCall in self.incomingCallId = incomingCall.callId self.isIncomingCall = true self.acceptCallButton.isHidden = false self.declineCallButton.isHidden = false } let onIncomingCallCancelled: (IncomingCallCancelled) -> Void = { [] _ in self.isIncomingCall = false self.acceptCallButton.isHidden = true self.declineCallButton.isHidden = true } callComposite.events.onRemoteParticipantJoined = onRemoteParticipantJoinedHandler callComposite.events.onError = onErrorHandler callComposite.events.onCallStateChanged = onCallStateChangedHandler callComposite.events.onDismissed = onDismissedHandler callComposite.events.onPictureInPictureChanged = onPipChangedHandler callComposite.events.onUserReportedIssue = onUserReportedIssueHandler callComposite.events.onIncomingCallAcceptedFromCallKit = callKitCallAccepted callComposite.events.onIncomingCall = onIncomingCall callComposite.events.onIncomingCallCancelled = onIncomingCallCancelled } func getLocalOptions(_ callComposite: CallComposite?) -> LocalOptions { let renderDisplayName = envConfigSubject.renderedDisplayName.isEmpty ? nil : envConfigSubject.renderedDisplayName let participantViewData = ParticipantViewData(avatar: UIImage(named: envConfigSubject.avatarImageName), displayName: renderDisplayName) let setupScreenViewData = SetupScreenViewData(title: envConfigSubject.navigationTitle, subtitle: envConfigSubject.navigationSubtitle) let cameraButton = ButtonViewData(onClick: { _ in print("::::UIKitDemoView::SetupScreen::onCameraButton::onClick") }) let micButton = ButtonViewData(onClick: { _ in print("::::UIKitDemoView::SetupScreen::onMicButton::onClick") }) let audioDeviceButton = ButtonViewData(onClick: { _ in print("::::UIKitDemoView::SetupScreen::onAudioDeviceButton::onClick") }) let setupScreenOptions = SetupScreenOptions(cameraButton: cameraButton, microphoneButton: micButton, audioDeviceButton: audioDeviceButton) let callScreenOptions = createCallScreenOptions(callComposite: callComposite) return LocalOptions(participantViewData: participantViewData, setupScreenViewData: setupScreenViewData, cameraOn: envConfigSubject.cameraOn, microphoneOn: envConfigSubject.microphoneOn, skipSetupScreen: envConfigSubject.skipSetupScreen, audioVideoMode: envConfigSubject.audioOnly ? .audioOnly : .audioAndVideo, setupScreenOptions: setupScreenOptions, callScreenOptions: callScreenOptions ) } private func createCallScreenOptions(callComposite: CallComposite?) -> CallScreenOptions { let cameraButton = ButtonViewData(visible: true, enabled: true) { _ in print("::::UIKitDemoView::CallScreen::cameraButton::onClick") } let micButton = ButtonViewData(visible: true, enabled: true) { _ in print("::::UIKitDemoView::CallScreen::micButton::onClick") } let audioDeviceButton = ButtonViewData(visible: true, enabled: true) { _ in print("::::UIKitDemoView::CallScreen::audioDeviceButton::onClick") } let liveCaptionsButton = ButtonViewData(visible: false, enabled: false) { _ in print("::::UIKitDemoView::CallScreen::liveCaptionsButton::onClick") } let liveCaptionsToggleButton = ButtonViewData(visible: false, enabled: false) { _ in print("::::UIKitDemoView::CallScreen::liveCaptionsToggleButton::onClick") } let spokenLanguageButton = ButtonViewData(visible: false, enabled: false) { _ in print("::::UIKitDemoView::CallScreen::spokenLanguageButton::onClick") } let captionsLanguageButton = ButtonViewData(visible: false, enabled: false) { _ in print("::::UIKitDemoView::CallScreen::captionsLanguageButton::onClick") } let shareDiagnosticsButton = ButtonViewData(visible: true, enabled: true) { _ in print("::::UIKitDemoView::CallScreen::shareDiagnosticsButton::onClick") } let reportIssueButton = ButtonViewData(visible: true, enabled: true) { _ in print("::::UIKitDemoView::CallScreen::reportIssueButton::onClick") } let customButton1 = CustomButtonViewData(id: UUID().uuidString, image: UIImage(named: "ic_fluent_chevron_right_20_regular")!, title: "Hide composite") {_ in print("::::UIKitDemoView::CallScreen::customButton1::onClick") callComposite?.isHidden = true } let callScreenControlBarOptions = CallScreenControlBarOptions( leaveCallConfirmationMode: envConfigSubject.displayLeaveCallConfirmation ? .alwaysEnabled : .alwaysDisabled, cameraButton: cameraButton, microphoneButton: micButton, audioDeviceButton: audioDeviceButton, liveCaptionsButton: liveCaptionsButton, liveCaptionsToggleButton: liveCaptionsToggleButton, spokenLanguageButton: spokenLanguageButton, captionsLanguageButton: captionsLanguageButton, shareDiagnosticsButton: shareDiagnosticsButton, reportIssueButton: reportIssueButton, customButtons: [customButton1] ) return CallScreenOptions(controlBarOptions: callScreenControlBarOptions) } func startCallWithDeprecatedLaunch() async { if let credential = try? await getTokenCredential(), let callComposite = try? await createCallComposite() { let link = getMeetingLink() var localOptions = getLocalOptions(nil) switch selectedMeetingType { case .groupCall: let uuid = UUID(uuidString: link) ?? UUID() callComposite.launch(remoteOptions: RemoteOptions(for: .groupCall(groupId: uuid), credential: credential, displayName: getDisplayName()), localOptions: localOptions) case .teamsMeeting: if !teamsMeetingTextField.text!.isEmpty { callComposite.launch( remoteOptions: RemoteOptions(for: .teamsMeeting(teamsLink: link), credential: credential, displayName: getDisplayName()), localOptions: localOptions ) } else if !teamsMeetingIdTextField.text!.isEmpty && !teamsMeetingPasscodeTextField.text!.isEmpty { callComposite.launch( remoteOptions: RemoteOptions(for: .teamsMeetingId(meetingId: teamsMeetingIdTextField.text!, meetingPasscode: teamsMeetingPasscodeTextField.text!), credential: credential, displayName: getDisplayName()), localOptions: localOptions ) } case.oneToNCall: let ids: [String] = link.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } let communicationIdentifiers: [CommunicationIdentifier] = ids.map { createCommunicationIdentifier(fromRawId: $0) } callComposite.launch(participants: communicationIdentifiers, localOptions: localOptions) case .roomCall: callComposite.launch(remoteOptions: RemoteOptions(for: .roomCall(roomId: link), credential: credential, displayName: getDisplayName()), localOptions: localOptions) } } } private func startExperience(with link: String) async { if let callComposite = try? await createCallComposite() { var remoteInfoDisplayName = envConfigSubject.callkitRemoteInfo if remoteInfoDisplayName.isEmpty { remoteInfoDisplayName = "ACS \(envConfigSubject.selectedMeetingType)" } let cxHandle = CXHandle(type: .generic, value: getCXHandleName()) let callKitRemoteInfo = envConfigSubject.enableRemoteInfo ? CallKitRemoteInfo(displayName: remoteInfoDisplayName, handle: cxHandle) : nil if envConfigSubject.useDeprecatedLaunch { await startCallWithDeprecatedLaunch() } else { let localOptions = getLocalOptions(callComposite) switch selectedMeetingType { case .groupCall: let uuid = UUID(uuidString: link) ?? UUID() callComposite.launch(locator: .groupCall(groupId: uuid), callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) case .teamsMeeting: if !link.isEmpty { callComposite.launch(locator: .teamsMeeting(teamsLink: link), callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) } else { callComposite.launch(locator: .teamsMeetingId(meetingId: envConfigSubject.teamsMeetingId, meetingPasscode: envConfigSubject.teamsMeetingPasscode), callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) } case.oneToNCall: let ids: [String] = link.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } let communicationIdentifiers: [CommunicationIdentifier] = ids.map { createCommunicationIdentifier(fromRawId: $0) } callComposite.launch(participants: communicationIdentifiers, callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) case .roomCall: callComposite.launch(locator: .roomCall(roomId: link), callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) } } } else { showError(for: DemoError.invalidToken.getErrorCode()) return } } private func getCallKitOptions() -> CallKitOptions { let cxHandle = CXHandle(type: .generic, value: getCXHandleName()) let providerConfig = CXProviderConfiguration() providerConfig.supportsVideo = true providerConfig.maximumCallGroups = 1 providerConfig.maximumCallsPerCallGroup = 1 providerConfig.includesCallsInRecents = true providerConfig.supportedHandleTypes = [.phoneNumber, .generic] let isCallHoldSupported = envConfigSubject.enableRemoteHold let callKitOptions = CallKitOptions(providerConfig: providerConfig, isCallHoldSupported: isCallHoldSupported, provideRemoteInfo: incomingCallRemoteInfo, configureAudioSession: configureAudioSession) return callKitOptions } public func incomingCallRemoteInfo(info: Caller) -> CallKitRemoteInfo { let cxHandle = CXHandle(type: .generic, value: "Incoming call") var remoteInfoDisplayName = envConfigSubject.callkitRemoteInfo if remoteInfoDisplayName.isEmpty { remoteInfoDisplayName = info.displayName } let callKitRemoteInfo = CallKitRemoteInfo(displayName: remoteInfoDisplayName, handle: cxHandle) return callKitRemoteInfo } public func configureAudioSession() -> Error? { let audioSession = AVAudioSession.sharedInstance() var configError: Error? do { try audioSession.setCategory(.playAndRecord) } catch { configError = error } return configError } private func getCXHandleName() -> String { switch envConfigSubject.selectedMeetingType { case .groupCall: return "Group call" case .teamsMeeting: return "Teams Metting" case .oneToNCall: return "Outgoing call" case .roomCall: return "Rooms call" } } private func getTokenCredential() async throws -> CommunicationTokenCredential { switch selectedAcsTokenType { case .token: if let communicationTokenCredential = try? CommunicationTokenCredential(token: acsTokenTextField.text!) { return communicationTokenCredential } else { throw DemoError.invalidToken } case .tokenUrl: if let url = URL(string: acsTokenUrlTextField.text!) { let tokenRefresher = AuthenticationHelper.getCommunicationToken(tokenUrl: url, aadToken: envConfigSubject.aadToken) let initialToken = await AuthenticationHelper.fetchInitialToken(with: tokenRefresher) let refreshOptions = CommunicationTokenRefreshOptions(initialToken: initialToken, refreshProactively: true, tokenRefresher: tokenRefresher) if let credential = try? CommunicationTokenCredential(withOptions: refreshOptions) { return credential } } throw DemoError.invalidToken } } private func getDisplayName() -> String { displayNameTextField.text ?? "" } private func getMeetingLink() -> String { switch selectedMeetingType { case .groupCall: return groupCallTextField.text ?? "" case .teamsMeeting: return teamsMeetingTextField.text ?? "" case .oneToNCall: return participantMRIsTextField.text ?? "" case .roomCall: return roomCallTextField.text ?? "" } } private func showError(for errorCode: String) { var errorMessage = "" switch errorCode { case CallCompositeErrorCode.tokenExpired: errorMessage = "Token is invalid" case CallCompositeErrorCode.microphonePermissionNotGranted: errorMessage = "Microphone Permission is denied" case CallCompositeErrorCode.networkConnectionNotAvailable: errorMessage = "Internet error" default: errorMessage = "Unknown error" } let errorAlert = UIAlertController(title: "Error", message: errorMessage, preferredStyle: .alert) errorAlert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)) present(errorAlert, animated: true, completion: nil) } private func showAlert(for message: String) { let alert = UIAlertController(title: "Message", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)) present(alert, animated: true, completion: nil) } private func registerNotifications() { let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) } private func updateUIBasedOnUserInterfaceStyle() { if UITraitCollection.current.userInterfaceStyle == .dark { view.backgroundColor = .black } else { view.backgroundColor = .white } } @objc func onAcsTokenTypeValueChanged(_ sender: UISegmentedControl!) { selectedAcsTokenType = ACSTokenType(rawValue: sender.selectedSegmentIndex)! updateAcsTokenTypeFields() } @objc func onMeetingTypeValueChanged(_ sender: UISegmentedControl!) { selectedMeetingType = MeetingType(rawValue: sender.selectedSegmentIndex)! updateMeetingTypeFields() } @objc func keyboardWillShow(notification: NSNotification) { isKeyboardShowing = true adjustScrollView() } @objc func keyboardWillHide(notification: NSNotification) { userIsEditing = false isKeyboardShowing = false adjustScrollView() } @objc func textFieldEditingDidChange() { startExperienceButton.isEnabled = !isStartExperienceDisabled updateStartExperieceButton() } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { userIsEditing = true return true } @objc func onSettingsPressed() { let settingsView = SettingsView(envConfigSubject: envConfigSubject) let settingsViewHostingController = UIHostingController(rootView: settingsView) settingsViewHostingController.modalPresentationStyle = .formSheet present(settingsViewHostingController, animated: true, completion: nil) } @objc func onShowHistoryBtnPressed() { let errorAlert = UIAlertController(title: callingViewModel.callHistoryTitle, message: callingViewModel.callHistoryMessage, preferredStyle: .alert) errorAlert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)) present(errorAlert, animated: true, completion: nil) } @objc func onStartExperienceBtnPressed() { startExperienceButton.isEnabled = false startExperienceButton.backgroundColor = .systemGray3 let link = self.getMeetingLink() Task { @MainActor in if getAudioPermissionStatus() == .denied && envConfigSubject.skipSetupScreen { showError(for: CallCompositeErrorCode.microphonePermissionNotGranted) startExperienceButton.isEnabled = true startExperienceButton.backgroundColor = .systemBlue return } await self.startExperience(with: link) startExperienceButton.isEnabled = true startExperienceButton.backgroundColor = .systemBlue } } @objc func onShowExperienceBtnPressed() { Task { await createCallComposite()?.isHidden = false } } @objc func onRegisterPushBtnPressed() { Task { await createCallComposite()? .registerPushNotifications( deviceRegistrationToken: envConfigSubject.deviceToken!) { result in switch result { case .success: self.showAlert(for: "Register Voip Success") case .failure(let error): self.showAlert(for: "Register Voip fail: \(error.localizedDescription)") } } } } @objc func onUnregisterPushBtnPressed() { Task { await createCallComposite()? .unregisterPushNotifications { result in switch result { case .success: self.showAlert(for: "Unregister Voip Success") case .failure(let error): self.showAlert(for: "Unregister Voip fail: \(error.localizedDescription)") } } } } @objc func onAcceptCallBtnPressed() { self.acceptCallButton.isHidden = true self.declineCallButton.isHidden = true Task { await createCallComposite()?.accept(incomingCallId: incomingCallId, localOptions: getLocalOptions(nil)) } } @objc func onDeclineCallBtnPressed() { self.acceptCallButton.isHidden = true self.declineCallButton.isHidden = true Task { await createCallComposite()?.reject(incomingCallId: incomingCallId) { result in switch result { case .success: self.showAlert(for: "Reject Success") case .failure(let error): self.showAlert(for: "Reject fail: \(error.localizedDescription)") } } } } func onPushNotificationReceived(dictionaryPayload: [AnyHashable: Any]) { let pushNotificationInfo = PushNotification(data: dictionaryPayload) if envConfigSubject.acsToken.isEmpty { self.envConfigSubject.load() } Task { await createCallComposite()?.handlePushNotification(pushNotification: pushNotificationInfo) } } private func updateAcsTokenTypeFields() { switch selectedAcsTokenType { case .tokenUrl: acsTokenUrlTextField.isHidden = false acsTokenTextField.isHidden = true case .token: acsTokenUrlTextField.isHidden = true acsTokenTextField.isHidden = false } } private func updateMeetingTypeFields() { switch selectedMeetingType { case .groupCall: groupCallTextField.isHidden = false teamsMeetingTextField.isHidden = true teamsMeetingIdTextField.isHidden = true teamsMeetingPasscodeTextField.isHidden = true participantMRIsTextField.isHidden = true roomCallTextField.isHidden = true case .teamsMeeting: groupCallTextField.isHidden = true teamsMeetingTextField.isHidden = false teamsMeetingIdTextField.isHidden = false teamsMeetingPasscodeTextField.isHidden = false participantMRIsTextField.isHidden = true roomCallTextField.isHidden = true case .roomCall: groupCallTextField.isHidden = true teamsMeetingTextField.isHidden = true teamsMeetingIdTextField.isHidden = true teamsMeetingPasscodeTextField.isHidden = true participantMRIsTextField.isHidden = true roomCallTextField.isHidden = false case .oneToNCall: groupCallTextField.isHidden = true teamsMeetingTextField.isHidden = true roomCallTextField.isHidden = true participantMRIsTextField.isHidden = false teamsMeetingIdTextField.isHidden = true teamsMeetingPasscodeTextField.isHidden = true } } private func updateStartExperieceButton() { if isStartExperienceDisabled { startExperienceButton.backgroundColor = .systemGray3 } else { startExperienceButton.backgroundColor = .systemBlue } } private var isStartExperienceDisabled: Bool { if (selectedAcsTokenType == .token && acsTokenTextField.text!.isEmpty) || (selectedAcsTokenType == .tokenUrl && acsTokenUrlTextField.text!.isEmpty) || (selectedMeetingType == .groupCall && groupCallTextField.text!.isEmpty) || (selectedMeetingType == .teamsMeeting && (teamsMeetingTextField.text!.isEmpty && (teamsMeetingIdTextField.text!.isEmpty || teamsMeetingPasscodeTextField.text!.isEmpty) )) || (selectedMeetingType == .roomCall && roomCallTextField.text!.isEmpty) { if (selectedAcsTokenType == .token && acsTokenTextField.text!.isEmpty) || (selectedAcsTokenType == .tokenUrl && acsTokenUrlTextField.text!.isEmpty) || (selectedMeetingType == .groupCall && groupCallTextField.text!.isEmpty) || (selectedMeetingType == .teamsMeeting && teamsMeetingTextField.text!.isEmpty) || (selectedMeetingType == .oneToNCall && participantMRIsTextField.text!.isEmpty) || (selectedMeetingType == .roomCall && roomCallTextField.text!.isEmpty) { return true } } return false } private func getAudioPermissionStatus() -> AVAudioSession.RecordPermission { return AVAudioSession.sharedInstance().recordPermission } private func setupUI() { updateUIBasedOnUserInterfaceStyle() let safeArea = view.safeAreaLayoutGuide #if DEBUG // Debug Buttons for Instrumentation to press // They shouldn't be visible let audioOnlyButton = UIButton(type: .system) audioOnlyButton.backgroundColor = UIColor.clear // Making the button transparent audioOnlyButton.addTarget(self, action: #selector(toggleAudioOnly), for: .touchUpInside) audioOnlyButton.accessibilityIdentifier = AccessibilityId.toggleAudioOnlyModeAccessibilityID.rawValue audioOnlyButton.frame = CGRect(x: 0, y: 0, width: 10, height: 10) // Minimal size let mockSdkButton = UIButton(type: .system) mockSdkButton.backgroundColor = UIColor.clear // Making the button transparent mockSdkButton.addTarget(self, action: #selector(toggleMockSdk), for: .touchUpInside) mockSdkButton.accessibilityIdentifier = AccessibilityId.useMockCallingSDKHandlerToggleAccessibilityID.rawValue mockSdkButton.frame = CGRect(x: 0, y: 0, width: 10, height: 10) // Minimal size let debugButtonsStackView = UIStackView(arrangedSubviews: [audioOnlyButton, mockSdkButton]) debugButtonsStackView.axis = .horizontal debugButtonsStackView.distribution = .fillEqually debugButtonsStackView.spacing = 4 // Reduced spacing debugButtonsStackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(debugButtonsStackView) NSLayoutConstraint.activate([ debugButtonsStackView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 8), debugButtonsStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 8), debugButtonsStackView.widthAnchor.constraint(equalToConstant: 24), // Container width audioOnlyButton.heightAnchor.constraint(equalToConstant: 10), // Button height mockSdkButton.heightAnchor.constraint(equalToConstant: 10) // Button height ]) #endif titleLabel = UILabel() titleLabel.text = "UI Library - UIKit Sample" titleLabel.sizeToFit() titleLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(titleLabel) titleLabelConstraint = titleLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: LayoutConstants.stackViewSpacingPortrait) titleLabelConstraint.isActive = true titleLabel.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor).isActive = true acsTokenUrlTextField = UITextField() acsTokenUrlTextField.placeholder = "ACS Token URL" acsTokenUrlTextField.text = envConfigSubject.acsTokenUrl acsTokenUrlTextField.delegate = self acsTokenUrlTextField.sizeToFit() acsTokenUrlTextField.translatesAutoresizingMaskIntoConstraints = false acsTokenUrlTextField.borderStyle = .roundedRect acsTokenUrlTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) acsTokenTextField = UITextField() acsTokenTextField.placeholder = "ACS Token" acsTokenTextField.text = envConfigSubject.acsToken acsTokenTextField.delegate = self acsTokenTextField.sizeToFit() acsTokenTextField.translatesAutoresizingMaskIntoConstraints = false acsTokenTextField.borderStyle = .roundedRect acsTokenTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) acsTokenTypeSegmentedControl = UISegmentedControl(items: ["Token URL", "Token"]) acsTokenTypeSegmentedControl.selectedSegmentIndex = envConfigSubject.selectedAcsTokenType.rawValue acsTokenTypeSegmentedControl.translatesAutoresizingMaskIntoConstraints = false acsTokenTypeSegmentedControl.addTarget(self, action: #selector(onAcsTokenTypeValueChanged(_:)), for: .valueChanged) selectedAcsTokenType = envConfigSubject.selectedAcsTokenType displayNameTextField = UITextField() displayNameTextField.placeholder = "Display Name" displayNameTextField.text = envConfigSubject.displayName displayNameTextField.translatesAutoresizingMaskIntoConstraints = false displayNameTextField.delegate = self displayNameTextField.borderStyle = .roundedRect displayNameTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) userIdTextField = UITextField() userIdTextField.placeholder = "User Identifier" userIdTextField.text = envConfigSubject.userId userIdTextField.translatesAutoresizingMaskIntoConstraints = false userIdTextField.delegate = self userIdTextField.borderStyle = .roundedRect userIdTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) groupCallTextField = UITextField() groupCallTextField.placeholder = "Group Call Id" groupCallTextField.text = envConfigSubject.groupCallId groupCallTextField.delegate = self groupCallTextField.sizeToFit() groupCallTextField.translatesAutoresizingMaskIntoConstraints = false groupCallTextField.borderStyle = .roundedRect groupCallTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) teamsMeetingTextField = UITextField() teamsMeetingTextField.placeholder = "Teams Meeting Link" teamsMeetingTextField.text = envConfigSubject.teamsMeetingLink teamsMeetingTextField.delegate = self teamsMeetingTextField.sizeToFit() teamsMeetingTextField.translatesAutoresizingMaskIntoConstraints = false teamsMeetingTextField.borderStyle = .roundedRect teamsMeetingTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) teamsMeetingIdTextField = UITextField() teamsMeetingIdTextField.placeholder = "Teams Meeting Id" teamsMeetingIdTextField.text = envConfigSubject.teamsMeetingId teamsMeetingIdTextField.delegate = self teamsMeetingIdTextField.sizeToFit() teamsMeetingIdTextField.translatesAutoresizingMaskIntoConstraints = false teamsMeetingIdTextField.borderStyle = .roundedRect teamsMeetingIdTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) teamsMeetingPasscodeTextField = UITextField() teamsMeetingPasscodeTextField.placeholder = "Teams Meeting Passcode" teamsMeetingPasscodeTextField.text = envConfigSubject.teamsMeetingPasscode teamsMeetingPasscodeTextField.delegate = self teamsMeetingPasscodeTextField.sizeToFit() teamsMeetingPasscodeTextField.translatesAutoresizingMaskIntoConstraints = false teamsMeetingPasscodeTextField.borderStyle = .roundedRect teamsMeetingPasscodeTextField.addTarget( self, action: #selector(textFieldEditingDidChange), for: .editingChanged) participantMRIsTextField = UITextField() participantMRIsTextField.placeholder = "Participant MRIs (, separated)" participantMRIsTextField.text = envConfigSubject.participantMRIs participantMRIsTextField.delegate = self participantMRIsTextField.sizeToFit() participantMRIsTextField.translatesAutoresizingMaskIntoConstraints = false participantMRIsTextField.borderStyle = .roundedRect participantMRIsTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) roomCallTextField = UITextField() roomCallTextField.placeholder = "Room Id" roomCallTextField.text = envConfigSubject.roomId roomCallTextField.delegate = self roomCallTextField.sizeToFit() roomCallTextField.translatesAutoresizingMaskIntoConstraints = false roomCallTextField.borderStyle = .roundedRect roomCallTextField.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) meetingTypeSegmentedControl = UISegmentedControl(items: ["Group Call", "Teams Meeting", "1:N", "Room Call"]) meetingTypeSegmentedControl.selectedSegmentIndex = envConfigSubject.selectedMeetingType.rawValue meetingTypeSegmentedControl.translatesAutoresizingMaskIntoConstraints = false meetingTypeSegmentedControl.addTarget(self, action: #selector(onMeetingTypeValueChanged(_:)), for: .valueChanged) selectedMeetingType = envConfigSubject.selectedMeetingType settingsButton = UIButton() settingsButton.setTitle("Settings", for: .normal) settingsButton.backgroundColor = .systemBlue settingsButton.addTarget(self, action: #selector(onSettingsPressed), for: .touchUpInside) settingsButton.layer.cornerRadius = 8 settingsButton.contentEdgeInsets = UIEdgeInsets.init(top: LayoutConstants.buttonVerticalInset, left: LayoutConstants.buttonHorizontalInset, bottom: LayoutConstants.buttonVerticalInset, right: LayoutConstants.buttonHorizontalInset) settingsButton.accessibilityIdentifier = AccessibilityId.settingsButtonAccessibilityID.rawValue showCallHistoryButton = UIButton() showCallHistoryButton.setTitle("Show call history", for: .normal) showCallHistoryButton.backgroundColor = .systemBlue showCallHistoryButton.contentEdgeInsets = UIEdgeInsets.init(top: LayoutConstants.buttonVerticalInset, left: LayoutConstants.buttonHorizontalInset, bottom: LayoutConstants.buttonVerticalInset, right: LayoutConstants.buttonHorizontalInset) showCallHistoryButton.layer.cornerRadius = 8 showCallHistoryButton.addTarget(self, action: #selector(onShowHistoryBtnPressed), for: .touchUpInside) startExperienceButton = UIButton() startExperienceButton.backgroundColor = .systemBlue startExperienceButton.setTitleColor(UIColor.white, for: .normal) startExperienceButton.setTitleColor(UIColor.systemGray6, for: .disabled) startExperienceButton.contentEdgeInsets = UIEdgeInsets.init(top: LayoutConstants.buttonVerticalInset, left: LayoutConstants.buttonHorizontalInset, bottom: LayoutConstants.buttonVerticalInset, right: LayoutConstants.buttonHorizontalInset) startExperienceButton.layer.cornerRadius = 8 startExperienceButton.setTitle("Start Experience", for: .normal) startExperienceButton.sizeToFit() startExperienceButton.translatesAutoresizingMaskIntoConstraints = false startExperienceButton.addTarget(self, action: #selector(onStartExperienceBtnPressed), for: .touchUpInside) startExperienceButton.accessibilityLabel = AccessibilityId.startExperienceAccessibilityID.rawValue showExperienceButton = UIButton() showExperienceButton.backgroundColor = .systemBlue showExperienceButton.setTitleColor(UIColor.white, for: .normal) showExperienceButton.setTitleColor(UIColor.systemGray6, for: .disabled) showExperienceButton.contentEdgeInsets = UIEdgeInsets.init(top: LayoutConstants.buttonVerticalInset, left: LayoutConstants.buttonHorizontalInset, bottom: LayoutConstants.buttonVerticalInset, right: LayoutConstants.buttonHorizontalInset) showExperienceButton.layer.cornerRadius = 8 showExperienceButton.setTitle("Show", for: .normal) showExperienceButton.sizeToFit() showExperienceButton.translatesAutoresizingMaskIntoConstraints = false showExperienceButton.addTarget(self, action: #selector(onShowExperienceBtnPressed), for: .touchUpInside) showExperienceButton.accessibilityLabel = AccessibilityId.showExperienceAccessibilityID.rawValue registerPushButton = UIButton() registerPushButton.backgroundColor = .systemBlue registerPushButton.setTitleColor(UIColor.white, for: .normal) registerPushButton.setTitleColor(UIColor.systemGray6, for: .disabled) registerPushButton.contentEdgeInsets = UIEdgeInsets.init(top: LayoutConstants.buttonVerticalInset, left: LayoutConstants.buttonHorizontalInset, bottom: LayoutConstants.buttonVerticalInset, right: LayoutConstants.buttonHorizontalInset) registerPushButton.layer.cornerRadius = 8 registerPushButton.setTitle("Register push", for: .normal) registerPushButton.sizeToFit() registerPushButton.translatesAutoresizingMaskIntoConstraints = false registerPushButton.addTarget(self, action: #selector(onRegisterPushBtnPressed), for: .touchUpInside) registerPushButton.accessibilityLabel = AccessibilityId.registerPushAccessibilityID.rawValue unregisterPushButton = UIButton() unregisterPushButton.backgroundColor = .systemBlue unregisterPushButton.setTitleColor(UIColor.white, for: .normal) unregisterPushButton.setTitleColor(UIColor.systemGray6, for: .disabled) unregisterPushButton.contentEdgeInsets = UIEdgeInsets.init(top: LayoutConstants.buttonVerticalInset, left: LayoutConstants.buttonHorizontalInset, bottom: LayoutConstants.buttonVerticalInset, right: LayoutConstants.buttonHorizontalInset) unregisterPushButton.layer.cornerRadius = 8 unregisterPushButton.setTitle("Unregister push", for: .normal) unregisterPushButton.sizeToFit() unregisterPushButton.translatesAutoresizingMaskIntoConstraints = false unregisterPushButton.addTarget(self, action: #selector(onUnregisterPushBtnPressed), for: .touchUpInside) unregisterPushButton.accessibilityLabel = AccessibilityId.unregisterPushAccessibilityID.rawValue acceptCallButton = UIButton() acceptCallButton.backgroundColor = .systemBlue acceptCallButton.setTitleColor(UIColor.white, for: .normal) acceptCallButton.setTitleColor(UIColor.systemGray6, for: .disabled) acceptCallButton.contentEdgeInsets = UIEdgeInsets.init(top: LayoutConstants.buttonVerticalInset, left: LayoutConstants.buttonHorizontalInset, bottom: LayoutConstants.buttonVerticalInset, right: LayoutConstants.buttonHorizontalInset) acceptCallButton.layer.cornerRadius = 8 acceptCallButton.setTitle("Accept", for: .normal) acceptCallButton.sizeToFit() acceptCallButton.translatesAutoresizingMaskIntoConstraints = false acceptCallButton.addTarget(self, action: #selector(onAcceptCallBtnPressed), for: .touchUpInside) acceptCallButton.accessibilityLabel = AccessibilityId.acceptCallAccessibilityID.rawValue declineCallButton = UIButton() declineCallButton.backgroundColor = .systemBlue declineCallButton.setTitleColor(UIColor.white, for: .normal) declineCallButton.setTitleColor(UIColor.systemGray6, for: .disabled) declineCallButton.contentEdgeInsets = UIEdgeInsets.init(top: LayoutConstants.buttonVerticalInset, left: LayoutConstants.buttonHorizontalInset, bottom: LayoutConstants.buttonVerticalInset, right: LayoutConstants.buttonHorizontalInset) declineCallButton.layer.cornerRadius = 8 declineCallButton.setTitle("Reject", for: .normal) declineCallButton.sizeToFit() declineCallButton.translatesAutoresizingMaskIntoConstraints = false declineCallButton.addTarget(self, action: #selector(onDeclineCallBtnPressed), for: .touchUpInside) declineCallButton.accessibilityLabel = AccessibilityId.declineCallAccessibilityID.rawValue callStateLabel = UILabel() callStateLabel.text = "State" callStateLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(callStateLabel) callStateLabelConstraint = callStateLabel.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: LayoutConstants.verticalSpacing) callStateLabelConstraint.isActive = true callStateLabel.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor).isActive = true callStateLabel.centerYAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -10).isActive = true // horizontal stack view for the settingButton and startExperienceButton let settingButtonHSpacer1 = UIView() settingButtonHSpacer1.translatesAutoresizingMaskIntoConstraints = false settingButtonHSpacer1.setContentHuggingPriority(.defaultLow, for: .horizontal) let settingButtonHSpacer2 = UIView() settingButtonHSpacer2.translatesAutoresizingMaskIntoConstraints = false settingButtonHSpacer2.setContentHuggingPriority(.defaultLow, for: .horizontal) let settingsButtonHStack = UIStackView(arrangedSubviews: [settingButtonHSpacer1, settingsButton, settingButtonHSpacer2]) settingsButtonHStack.axis = .horizontal settingsButtonHStack.alignment = .fill settingsButtonHStack.distribution = .fill settingsButtonHStack.translatesAutoresizingMaskIntoConstraints = false let showHistoryButtonHSpacer1 = UIView() showHistoryButtonHSpacer1.translatesAutoresizingMaskIntoConstraints = false showHistoryButtonHSpacer1.setContentHuggingPriority(.defaultLow, for: .horizontal) let showHistoryButtonHSpacer2 = UIView() showHistoryButtonHSpacer2.translatesAutoresizingMaskIntoConstraints = false showHistoryButtonHSpacer2.setContentHuggingPriority(.defaultLow, for: .horizontal) let showHistoryButtonHStack = UIStackView(arrangedSubviews: [showHistoryButtonHSpacer1, showCallHistoryButton, showHistoryButtonHSpacer2]) showHistoryButtonHStack.axis = .horizontal showHistoryButtonHStack.alignment = .fill showHistoryButtonHStack.distribution = .fill showHistoryButtonHStack.translatesAutoresizingMaskIntoConstraints = false let startCallButtonHSpacer1 = UIView() startCallButtonHSpacer1.translatesAutoresizingMaskIntoConstraints = false startCallButtonHSpacer1.setContentHuggingPriority(.defaultLow, for: .horizontal) let startCallButtonHSpacer2 = UIView() startCallButtonHSpacer2.translatesAutoresizingMaskIntoConstraints = false startCallButtonHSpacer2.setContentHuggingPriority(.defaultLow, for: .horizontal) let startCallButtonHStack = UIStackView(arrangedSubviews: [startCallButtonHSpacer1, startExperienceButton, startCallButtonHSpacer2]) startCallButtonHStack.axis = .horizontal startCallButtonHStack.alignment = .fill startCallButtonHStack.distribution = .fill startCallButtonHStack.translatesAutoresizingMaskIntoConstraints = false let startButtonHSpacer1 = UIView() startButtonHSpacer1.translatesAutoresizingMaskIntoConstraints = false startButtonHSpacer1.setContentHuggingPriority(.defaultLow, for: .horizontal) let startButtonHSpacer2 = UIView() startButtonHSpacer2.translatesAutoresizingMaskIntoConstraints = false startButtonHSpacer2.setContentHuggingPriority(.defaultLow, for: .horizontal) let startButtonHStack = UIStackView(arrangedSubviews: [startButtonHSpacer1, startExperienceButton, startButtonHSpacer2]) startButtonHStack.axis = .horizontal startButtonHStack.alignment = .fill startButtonHStack.distribution = .fill startButtonHStack.translatesAutoresizingMaskIntoConstraints = false let showButtonHSpacer1 = UIView() showButtonHSpacer1.translatesAutoresizingMaskIntoConstraints = false showButtonHSpacer1.setContentHuggingPriority(.defaultLow, for: .horizontal) let showButtonHSpacer2 = UIView() showButtonHSpacer2.translatesAutoresizingMaskIntoConstraints = false showButtonHSpacer2.setContentHuggingPriority(.defaultLow, for: .horizontal) let showButtonHStack = UIStackView(arrangedSubviews: [showButtonHSpacer1, showExperienceButton, showButtonHSpacer2]) showButtonHStack.axis = .horizontal showButtonHStack.alignment = .fill showButtonHStack.distribution = .fill showButtonHStack.translatesAutoresizingMaskIntoConstraints = false let spaceView1 = UIView() spaceView1.translatesAutoresizingMaskIntoConstraints = false spaceView1.heightAnchor.constraint(equalToConstant: 0).isActive = true let registerUnregisterHStack = UIStackView(arrangedSubviews: [registerPushButton, unregisterPushButton]) registerUnregisterHStack.axis = .horizontal registerUnregisterHStack.distribution = .fillEqually // Adjust distribution as needed registerUnregisterHStack.spacing = 8 let acceptDeclineHStack = UIStackView(arrangedSubviews: [acceptCallButton, declineCallButton]) acceptDeclineHStack.axis = .horizontal acceptDeclineHStack.distribution = .fillEqually // Adjust distribution as needed acceptDeclineHStack.spacing = 8 stackView = UIStackView(arrangedSubviews: [spaceView1, acsTokenTypeSegmentedControl, acsTokenUrlTextField, acsTokenTextField, displayNameTextField, userIdTextField, meetingTypeSegmentedControl, groupCallTextField, teamsMeetingTextField, teamsMeetingIdTextField, teamsMeetingPasscodeTextField, participantMRIsTextField, roomCallTextField, settingsButtonHStack, showHistoryButtonHStack, startButtonHStack, showButtonHStack, registerUnregisterHStack, acceptDeclineHStack]) stackView.spacing = LayoutConstants.stackViewSpacingPortrait stackView.axis = .vertical stackView.alignment = .fill stackView.distribution = .fill stackView.translatesAutoresizingMaskIntoConstraints = false stackView.setCustomSpacing(0, after: stackView.arrangedSubviews.first!) view.addSubview(scrollView) scrollView.addSubview(contentView) scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: LayoutConstants.verticalSpacing).isActive = true scrollView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor).isActive = true scrollView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true scrollView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true contentView.addSubview(stackView) contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true stackView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: LayoutConstants.stackViewSpacingPortrait).isActive = true stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -LayoutConstants.stackViewSpacingPortrait).isActive = true stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true settingButtonHSpacer2.widthAnchor.constraint(equalTo: settingButtonHSpacer1.widthAnchor).isActive = true showHistoryButtonHSpacer2.widthAnchor.constraint(equalTo: showHistoryButtonHSpacer1.widthAnchor).isActive = true startButtonHSpacer2.widthAnchor.constraint(equalTo: startButtonHSpacer1.widthAnchor).isActive = true showButtonHSpacer2.widthAnchor.constraint(equalTo: showButtonHSpacer1.widthAnchor).isActive = true updateAcsTokenTypeFields() updateMeetingTypeFields() startExperienceButton.isEnabled = !isStartExperienceDisabled updateStartExperieceButton() self.acceptCallButton.isHidden = true self.declineCallButton.isHidden = true } private func adjustScrollView() { if UIDevice.current.userInterfaceIdiom == .phone || UIDevice.current.orientation.isLandscape { if self.isKeyboardShowing { let offset: CGFloat = (UIDevice.current.orientation.isPortrait || UIDevice.current.orientation == .unknown) ? 200 : 250 let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: offset, right: 0) scrollView.contentInset = contentInsets scrollView.scrollIndicatorInsets = contentInsets scrollView.setContentOffset(CGPoint(x: 0, y: offset), animated: true) } else { scrollView.contentInset = .zero scrollView.scrollIndicatorInsets = .zero } } } @objc func toggleAudioOnly() { envConfigSubject.audioOnly = !envConfigSubject.audioOnly } @objc func toggleMockSdk() { envConfigSubject.useMockCallingSDKHandler = !envConfigSubject.useMockCallingSDKHandler } } extension CallingDemoViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return false } }