- Set Permissions and Add Frameworks and Libraries
- Design the User Interface
- Create the MainViewController Class
- Create the MainViewController Class Delegates
- Create the RoomViewController
- Create RoomViewController Agora Methods and Delegates
- Create the ChatMessageViewController
- Create the SettingsViewController
Under the Capabilities tab, enable Audio, AirPlay, and Picture in Picture mode.
Open the info.plist file. Enable the camera and microphone privacy settings for the application.
Under the Build Phases tab, add the following frameworks and libraries to your project:
SystemConfiguration.frameworklibresolv.tbdAgoraRtcEngineKit.frameworkCoreTelephony.frameworkCoreMedia.frameworkVideoToolbox.frameworkAudioToolbox.frameworklibc++.tbdAgoraRtcCryptoLoader.frameworkAVFoundation.frameworklibcrypto.a
- Add Assets
- Create the MainViewController UI
- Create the RoomViewController UI and the ChatMessageViewController UI
- Create the SettingsViewController UI
Add the following assets to Assets.xcassets.
Note: Use Xcode to import assets to Assets.xcassets. PDF files are used for these assets, which contain images for each iOS screen resolution.
| Asset | Description |
|---|---|
btn_back and btn_next_black |
Image of a left and right arrow for navigation |
btn_cutaways |
An image of a camera and rotational arrows to switch between the two cameras |
btn_endcall |
An image of a red telephone for the hang up button |
btn_filter and btn_filter_blue |
Images of glasses for filtering |
btn_keyboard_hide |
An image of a down arrow used to hide/show the visual keyboard |
btn_message and btn_message_blue |
Images of chat bubbles to initiate a call |
btn_mute and btn_mute_blue |
Images of a microphone to mute/unmute audio |
btn_setting |
An image of a cog to open the settings window |
btn_speaker and btn_speaker_blue |
Images of speakers to turn audio on/off |
btn_video |
An image of a camera to start video |
btn_voice |
An image of an arrow indicating that audio chat is enabled |
Create the layout for the MainViewController.
Note: This layout includes navigation segues to move from screen to screen.
Create the layout for the RoomViewController and ChatMessageViewController. The ChatMessageViewController view is embedded in the RoomViewController view.
Note: The RoomViewController layout includes tap and double-tap gesture recognizers for handling user interaction.
Create the layout for the SettingsViewController.
MainViewController.swift defines and connects application functionality with the MainViewController UI.
- Define Global Variables
- Override the prepare() Segue Method
- Create the doRoomNameTextFieldEditing() IBAction Method
- Create the doEncryptionTextFieldEditing() IBAction Method
- Create the doEncryptionTypePressed() IBAction Method
- Create the doJoinPressed() IBAction Method
- Create the enter() Method
The MainViewController class has three IBOutlet variables. These map to the MainViewController UI elements.
| Variable | Description |
|---|---|
roomNameTextField |
Maps to the Channel name UITextField in the MainViewController layout |
encryptionTextField |
Maps to the Encryption key UITextField in the MainViewController layout |
encryptionButton |
Maps to the AES 128 UIButton in the MainViewController layout |
import UIKit
class MainViewController: UIViewController {
@IBOutlet weak var roomNameTextField: UITextField!
@IBOutlet weak var encryptionTextField: UITextField!
@IBOutlet weak var encryptionButton: UIButton!
...
}The MainViewController class has two private variables.
The videoProfile variable is initialized with the default Agora video profile using AgoraVideoProfile.defaultProfile().
The encryptionType is initialized to EncryptionType.xts128. When a new encryptionType is set, set the encryptionButton text to the new encryption type using encryptionButton?.setTitle().
fileprivate var videoProfile = AgoraVideoProfile.defaultProfile()
fileprivate var encryptionType = EncryptionType.xts128 {
didSet {
encryptionButton?.setTitle(encryptionType.description(), for: UIControlState())
}
}Override the prepare() segue method to manage the application navigation.
If the segueId is mainToSettings, prepare the settings view through the segue destination SettingsViewController:
- Set
settingsVC.videoProfileto the currentvideoProfile. - Set
settingsVC.delegatetoself.
If the segueId is mainToRoom , prepare the room view through the segue destination RoomViewController:
- Set
roomVC.roomNametosender. - Set
roomVC.encryptionSecretto the text entered in theencryptionTextField. - Set
roomVC.encryptionTypeto the currentencryptionType. - Set
roomVC.videoProfileto the currentvideoProfile. - Set
roomVC.delegatetoself.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let segueId = segue.identifier else {
return
}
switch segueId {
case "mainToSettings":
let settingsVC = segue.destination as! SettingsViewController
settingsVC.videoProfile = videoProfile
settingsVC.delegate = self
case "mainToRoom":
let roomVC = segue.destination as! RoomViewController
roomVC.roomName = (sender as! String)
roomVC.encryptionSecret = encryptionTextField.text
roomVC.encryptionType = encryptionType
roomVC.videoProfile = videoProfile
roomVC.delegate = self
default:
break
}
}The doRoomNameTextFieldEditing() IBAction method is invoked by roomNameTextField. When the UITextField text is edited, format the text using MediaCharacter.updateToLegalMediaString().
@IBAction func doRoomNameTextFieldEditing(_ sender: UITextField) {
if let text = sender.text , !text.isEmpty {
let legalString = MediaCharacter.updateToLegalMediaString(from: text)
sender.text = legalString
}
}The doEncryptionTextFieldEditing() IBAction method is invoked by encryptionTextField. When the UITextField text is edited, format the text using MediaCharacter.updateToLegalMediaString().
@IBAction func doEncryptionTextFieldEditing(_ sender: UITextField) {
if let text = sender.text , !text.isEmpty {
let legalString = MediaCharacter.updateToLegalMediaString(from: text)
sender.text = legalString
}
}The doEncryptionTypePressed() IBAction method is invoked by encryptionButton. When the UIButton is pressed, create a popover UI object using UIAlertController():
-
Create a
UIAlertActionobject for each encryption type and add it tosheet. -
Create a
cancelUIAlertActionobject and add it tosheet. -
Apply the popover above
encryptionButtonby settingsheet.popoverPresentationController?.sourceViewand settingsheet.popoverPresentationController?.permittedArrowDirectionsto.up. -
Display the pop using
present().
@IBAction func doEncryptionTypePressed(_ sender: UIButton) {
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
for encryptionType in EncryptionType.allValue {
let action = UIAlertAction(title: encryptionType.description(), style: .default) { [weak self] _ in
self?.encryptionType = encryptionType
}
sheet.addAction(action)
}
let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
sheet.addAction(cancel)
sheet.popoverPresentationController?.sourceView = encryptionButton
sheet.popoverPresentationController?.permittedArrowDirections = .up
present(sheet, animated: true, completion: nil)
}The JoinChannel UI Button in the MainViewController layout invokes the doJoinPressed() IBAction method. This method enters the user into the room specified by roomNameTextField using enter().
@IBAction func doJoinPressed(_ sender: UIButton) {
enter(roomName: roomNameTextField.text)
}The enter() method ensures the room name is valid before navigating to the room view using performSegue().
private extension MainViewController {
func enter(roomName: String?) {
guard let roomName = roomName , !roomName.isEmpty else {
return
}
performSegue(withIdentifier: "mainToRoom", sender: roomName)
}
}The settingsVC method is a delegate method for the SettingsViewController. This method is invoked when the video profile for the SettingsViewController changes. It updates the videoProfile, and dismisses the view using dismiss().
extension MainViewController: SettingsVCDelegate {
func settingsVC(_ settingsVC: SettingsViewController, didSelectProfile profile: AgoraVideoProfile) {
videoProfile = profile
dismiss(animated: true, completion: nil)
}
}The roomVCNeedClose method is a delegate method for the RoomVCDelegate. This method is invoked when the user leaves the room, and dismisses the view using dismiss().
extension MainViewController: RoomVCDelegate {
func roomVCNeedClose(_ roomVC: RoomViewController) {
dismiss(animated: true, completion: nil)
}
}The textFieldShouldReturn method is a delegate method for the UITextField objects in MainViewController. This method is invoked when the user presses the keyboard return.
- If the current text field is
roomNameTextField, enter the user into the specified room usingenter. - If the current text field is
encryptionTextField, dismiss the keyboard usingtextField.resignFirstResponder().
extension MainViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
switch textField {
case roomNameTextField: enter(roomName: textField.text)
case encryptionTextField: textField.resignFirstResponder()
default: break
}
return true
}
}RoomViewController.swift defines and connects application functionality with the RoomViewController UI.
- Define the RoomVCDelegate Protocol
- Define Global Variables
- Define IBOutlet Variables
- Define Private Class Variables
- Create Delegate and Superclass Methods
- Create IBAction Methods
- Create Private Methods
The roomVCNeedClose() method is used for communication between the RoomViewController class and its delegate. The method informs the delegate to close the room.
import UIKit
protocol RoomVCDelegate: class {
func roomVCNeedClose(_ roomVC: RoomViewController)
}The RoomViewController class has five public variables. These variables manage the RoomViewController settings.
| Variable | Description |
|---|---|
roomName |
The name of the room |
encryptionSecret |
The encryption key for the room |
encryptionType |
The encryption type for the room |
videoProfile |
The video profile for the room |
delegate |
The delegate for the RoomViewController class |
AgoraRtcEngineKit |
The Agora RTC Engine SDK object |
//MARK: public var
var roomName: String!
var encryptionSecret: String?
var encryptionType: EncryptionType!
var videoProfile: AgoraVideoProfile!
weak var delegate: RoomVCDelegate?
...
var agoraKit: AgoraRtcEngineKit!
...The RoomViewController class has IBOutlet variables to manage buttons, view containers, and handle other UI elements. The variables map to the RoomViewController UI elements.
| Variable | Description |
|---|---|
containerView |
Container for the videos in the room |
flowViews |
Set of key UI elements that need to be visually managed by the controller |
roomNameLabel |
Label for the room name in the header of the layout |
controlView |
Container for the room control buttons |
messageTableContainerView |
List of messages |
messageButton |
Button for messaging |
muteVideoButton |
Button to mute/unmute the video |
muteAudioButton |
Button to mute/unmute the audio |
cameraButton |
Button for the camera |
speakerButton |
Button for the speakerphone |
filterButton |
Button for filtering |
messageInputerView |
Container for message creation |
messageInputerBottom |
Layout constraint for the message creation container |
messageTextField |
Text field for the message creation |
backgroundTap |
Single-tap gesture recognizer |
backgroundDoubleTap |
Double-tap gesture recognizer |
class RoomViewController: UIViewController {
//MARK: IBOutlet
@IBOutlet weak var containerView: UIView!
@IBOutlet var flowViews: [UIView]!
@IBOutlet weak var roomNameLabel: UILabel!
@IBOutlet weak var controlView: UIView!
@IBOutlet weak var messageTableContainerView: UIView!
@IBOutlet weak var messageButton: UIButton!
@IBOutlet weak var muteVideoButton: UIButton!
@IBOutlet weak var muteAudioButton: UIButton!
@IBOutlet weak var cameraButton: UIButton!
@IBOutlet weak var speakerButton: UIButton!
@IBOutlet weak var filterButton: UIButton!
@IBOutlet weak var messageInputerView: UIView!
@IBOutlet weak var messageInputerBottom: NSLayoutConstraint!
@IBOutlet weak var messageTextField: UITextField!
@IBOutlet var backgroundTap: UITapGestureRecognizer!
@IBOutlet var backgroundDoubleTap: UITapGestureRecognizer!
...
} - UI Management Variables
- Video Session Variables
- Audio and Video Control Variables
- Chat Message Control Variables
The shouldHideFlowViews variable defaults to false. When this variable changes, the flowViews are hidden/not hidden.
The videoViewLayouter variable is initialized by default and handles the layout for the video views.
//MARK: hide & show
fileprivate var shouldHideFlowViews = false {
didSet {
if let flowViews = flowViews {
for view in flowViews {
view.isHidden = shouldHideFlowViews
}
}
}
}
...
fileprivate let videoViewLayouter = VideoViewLayouter()
...
//MARK: alert
fileprivate weak var currentAlert: UIAlertController?
...
The videoSessions and doubleClickFullSession variables handle the video sessions for the room.
When videoSessions is set, update the interface with the video sessions using updateInterface().
When doubleClickFullSession is set, update the interface with the video sessions using updateInterface() if (1) the number of sessions is 3 or more, and (2) the interface has not already been updated (to avoid duplication).
The dataChannelId is set to -1 by default and manages the room channel.
The currentAlert is not set by default and is available for use to display alerts to the user.
//MARK: engine & session
...
fileprivate var videoSessions = [VideoSession]() {
didSet {
updateInterface(with: self.videoSessions, targetSize: containerView.frame.size, animation: true)
}
}
fileprivate var doubleClickFullSession: VideoSession? {
didSet {
if videoSessions.count >= 3 && doubleClickFullSession != oldValue {
updateInterface(with: videoSessions, targetSize: containerView.frame.size, animation: true)
}
}
}
...
fileprivate var dataChannelId: Int = -1
...
The audioMuted and videoMuted variables are set to false by default, and manage the audio and video streams, respectively.
When audioMuted is set, the muteAudioButton image is updated, and the audio stream is muted/unmuted using agoraKit.muteLocalAudioStream().
When videoMuted is set:
- The
muteVideoButtonimage is updated. - The
cameraButtonandspeakerButtonare set to hidden/not hidden. - The video stream is stopped/started using
agoraKit.muteLocalVideoStream()andsetVideoMuted(). - The video view of the current user is set to hidden/not hidden using
updateSelfViewVisiable().
//MARK: mute
fileprivate var audioMuted = false {
didSet {
muteAudioButton?.setImage(UIImage(named: audioMuted ? "btn_mute_blue" : "btn_mute"), for: UIControlState())
agoraKit.muteLocalAudioStream(audioMuted)
}
}
fileprivate var videoMuted = false {
didSet {
muteVideoButton?.setImage(UIImage(named: videoMuted ? "btn_video" : "btn_voice"), for: UIControlState())
cameraButton?.isHidden = videoMuted
speakerButton?.isHidden = !videoMuted
agoraKit.muteLocalVideoStream(videoMuted)
setVideoMuted(videoMuted, forUid: 0)
updateSelfViewVisiable()
}
}The speakerEnabled variable is set to true by default. When this variable is set, the speakerButton image is updated and the speakerphone is enabled/disabled using agoraKit.setEnableSpeakerphone().
//MARK: speaker
fileprivate var speakerEnabled = true {
didSet {
speakerButton?.setImage(UIImage(named: speakerEnabled ? "btn_speaker_blue" : "btn_speaker"), for: UIControlState())
speakerButton?.setImage(UIImage(named: speakerEnabled ? "btn_speaker" : "btn_speaker_blue"), for: .highlighted)
agoraKit.setEnableSpeakerphone(speakerEnabled)
}
}The isFiltering variable is set to false by default. When this variable is set:
- The creation of
agoraKitis verified. - If filtering is enabled, set the video preprocessing using
AGVideoPreProcessing.registerVideoPreprocessing()and update thefilterButtonwith the blue image. - If filtering is not enabled, update the
filterButtonwith the white image.
//MARK: filter
fileprivate var isFiltering = false {
didSet {
guard let agoraKit = agoraKit else {
return
}
if isFiltering {
AGVideoPreProcessing.registerVideoPreprocessing(agoraKit)
filterButton?.setImage(UIImage(named: "btn_filter_blue"), for: UIControlState())
} else {
AGVideoPreProcessing.deregisterVideoPreprocessing(agoraKit)
filterButton?.setImage(UIImage(named: "btn_filter"), for: UIControlState())
}
}
}The chatMessageVC variable manages the chat message list.
The isInputing variable is set to false as the default. When this is set:
- the
messageTextFieldis activated/deactivated usingbecomeFirstResponder()/resignFirstResponder(). - the
messageInputerViewis hidden/unhidden. - the
messageButtonimage is updated usingmessageButton?.setImage().
//MARK: text message
fileprivate var chatMessageVC: ChatMessageViewController?
fileprivate var isInputing = false {
didSet {
if isInputing {
messageTextField?.becomeFirstResponder()
} else {
messageTextField?.resignFirstResponder()
}
messageInputerView?.isHidden = !isInputing
messageButton?.setImage(UIImage(named: isInputing ? "btn_message_blue" : "btn_message"), for: UIControlState())
}
}Initialize cryptoLoader using AgoraRtcCryptoLoader(). This object manages Agora encryption.
//MARK: crypto loader
private let cryptoLoader = AgoraRtcCryptoLoader()The viewDidLoad() method initializes the RoomViewController:
- Set the
roomNameLabeltext. - Set the
backgroundTapgesture recognizer to fail on double-tap usingbackgroundTap.require(). - Add the keyboard event listener using
addKeyboardObserver(). - Load the Agora RTC engine SDK using
loadAgoraKit().
//MARK: - life cycle
override func viewDidLoad() {
super.viewDidLoad()
roomNameLabel.text = "\(roomName!)"
backgroundTap.require(toFail: backgroundDoubleTap)
addKeyboardObserver()
loadAgoraKit()
}The prepare() segue method manages the navigation for the RoomViewController. If the segueId is VideoVCEmbedChatMessageVC, set chatMessageVC to the ChatMessageViewController; otherwise do nothing.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let segueId = segue.identifier else {
return
}
switch segueId {
case "VideoVCEmbedChatMessageVC":
chatMessageVC = segue.destination as? ChatMessageViewController
default:
break
}
}The viewWillTransition() method sets the size for each session in the videoSessions array and updates the interface with the new sizes using updateInterface().
//MARK: - rotation
extension RoomViewController {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
for session in videoSessions {
if let sessionSize = session.size {
session.size = sessionSize.fixedSize(with: size)
}
}
updateInterface(with: videoSessions, targetSize: size, animation: true)
}
}Return .all for supportedInterfaceOrientations to allow the sample application to support the device in any orientation.
override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
return .all
}The textFieldShouldReturn() delegate method applies to all UITextField elements in the RoomViewController layout. This method is invoked when the keyboard return is pressed while editing the text fields.
If the text field is not empty, invoke the send() method, and clear the text in the text field.
//MARK: - textFiled
extension RoomViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if let text = textField.text , !text.isEmpty {
send(text: text)
textField.text = nil
}
return true
}
}These IBAction methods map to the UI elements for the RoomViewController:
- Message Methods
- Video and Audio Methods
- Camera, Speaker, Filter, and Close Methods
- Gesture Recognizer Methods
The doMessagePressed() method is invoked by the messageButton UI button and updates isInputing.
The doCloseMessagePressed() method is invoked by the the UI button with the chat bubble image and sets isInputing to false.
//MARK: - user action
@IBAction func doMessagePressed(_ sender: UIButton) {
isInputing = !isInputing
}
@IBAction func doCloseMessagePressed(_ sender: UIButton) {
isInputing = false
}The doMuteVideoPressed() method is invoked by the muteVideoButton UI button and updates videoMuted.
The doMuteAudioPressed() method is invoked by the muteAudioButton UI button and updates audioMuted.
@IBAction func doMuteVideoPressed(_ sender: UIButton) {
videoMuted = !videoMuted
}
@IBAction func doMuteAudioPressed(_ sender: UIButton) {
audioMuted = !audioMuted
}The doCameraPressed() method is invoked by the cameraButton UI button action and switches the camera view using agoraKit.switchCamera().
The doSpeakerPressed() method is invoked by the speakerButton UI button action and updates speakerEnabled.
The doFilterPressed() method is invoked by the filterButton UI button action and updates isFiltering.
The doClosePressed() method is invoked by the red hangup UI button action and invokes the leaveChannel() method.
@IBAction func doCameraPressed(_ sender: UIButton) {
agoraKit.switchCamera()
}
@IBAction func doSpeakerPressed(_ sender: UIButton) {
speakerEnabled = !speakerEnabled
}
@IBAction func doFilterPressed(_ sender: UIButton) {
isFiltering = !isFiltering
}
@IBAction func doClosePressed(_ sender: UIButton) {
leaveChannel()
}The doBackTapped() method is invoked by the backgroundTap gesture recognizer and updates shouldHideFlowViews.
The doBackDoubleTapped() method is invoked by the backgroundDoubleTap gesture recognizer.
- If
doubleClickFullSessionisnil, detect the video session index, and setdoubleClickFullSessionto the selected video session. - If
doubleClickFullSessionalready exists, setdoubleClickFullSessionto nil.
@IBAction func doBackTapped(_ sender: UITapGestureRecognizer) {
if !isInputing {
shouldHideFlowViews = !shouldHideFlowViews
}
}
@IBAction func doBackDoubleTapped(_ sender: UITapGestureRecognizer) {
if doubleClickFullSession == nil {
//将双击到的session全屏
if let tappedIndex = videoViewLayouter.reponseViewIndex(of: sender.location(in: containerView)) {
doubleClickFullSession = videoSessions[tappedIndex]
}
} else {
doubleClickFullSession = nil
}
}The private methods for the RoomViewController are created as functions in a private extension.
//MARK: - private
private extension RoomViewController {
...
}- Create the addKeyboardObserver() Method
- Create the updateInterface() Methods
- Create Session Methods
- Create the UI Control Methods
They addKeyboardObserver() method adds event listeners for keyboard events.
The UIKeyboardWillShow event is triggered before the keyboard is displayed.
The strongSelf, keyBoardBoundsValue, and durationValue variables are set from the notifications userInfo. If userInfo does not contain the relevant values, the callback is ended with a return. These values are used to set the following local variables:
| Variable | Description |
|---|---|
keyBoardBounds |
Keyboard dimensions |
duration |
Animation time |
deltaY |
Keyboard height. Used to measure the keyboard vertical animation distance. |
To keep the message box above the keyboard:
-
If the
durationis greater than0, animatestrongSelf.messageInputerBottomusing the animation typeUIKeyboardAnimationCurveUserInfoKeyif available. -
If the
durationis less than or equal to0, setstrongSelf.messageInputerBottom.constanttodeltaY.
func addKeyboardObserver() {
NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillShow, object: nil, queue: nil) { [weak self] notify in
guard let strongSelf = self, let userInfo = (notify as NSNotification).userInfo,
let keyBoardBoundsValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue,
let durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber else {
return
}
let keyBoardBounds = keyBoardBoundsValue.cgRectValue
let duration = durationValue.doubleValue
let deltaY = keyBoardBounds.size.height
if duration > 0 {
var optionsInt: UInt = 0
if let optionsValue = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
optionsInt = optionsValue.uintValue
}
let options = UIViewAnimationOptions(rawValue: optionsInt)
UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
strongSelf.messageInputerBottom.constant = deltaY
strongSelf.view?.layoutIfNeeded()
}, completion: nil)
} else {
strongSelf.messageInputerBottom.constant = deltaY
}
}
...
}The UIKeyboardWillHide keyboard event is triggered before the keyboard is hidden from the screen.
-
Set
userInfofrom the notification'suserInfoand extractdurationValue/duration. -
Return the message box to the bottom of the screen:
-
If the
durationis greater than0, animatestrongSelf.messageInputerBottomusing the animation typeUIKeyboardAnimationCurveUserInfoKeyif available. -
If the
durationis less than or equal to0, setstrongSelf.messageInputerBottom.constantto0.
NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillHide, object: nil, queue: nil) { [weak self] notify in
guard let strongSelf = self else {
return
}
let duration: Double
if let userInfo = (notify as NSNotification).userInfo, let durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber {
duration = durationValue.doubleValue
} else {
duration = 0
}
if duration > 0 {
var optionsInt: UInt = 0
if let userInfo = (notify as NSNotification).userInfo, let optionsValue = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
optionsInt = optionsValue.uintValue
}
let options = UIViewAnimationOptions(rawValue: optionsInt)
UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
strongSelf.messageInputerBottom.constant = 0
strongSelf.view?.layoutIfNeeded()
}, completion: nil)
} else {
strongSelf.messageInputerBottom.constant = 0
}
}The updateInterface() methods handle layout updates for the video session.
- The
updateInterface()method withanimationchecks if animation is used for the update, and animates the update within 0.3 seconds usingUIView.animate().
func updateInterface(with sessions: [VideoSession], targetSize: CGSize, animation: Bool) {
if animation {
UIView.animate(withDuration: 0.3, delay: 0, options: .beginFromCurrentState, animations: {[weak self] () -> Void in
self?.updateInterface(with: sessions, targetSize: targetSize)
self?.view.layoutIfNeeded()
}, completion: nil)
} else {
updateInterface(with: sessions, targetSize: targetSize)
}
}- The
updateInterface()method without animation sets the location forvideoViewLayouterand video views.
Loop through sessions to retrieve each hostingView and append the view to peerVideoViews:
- Apply
peerVideoViewsto the video layout manager usingvideoViewLayouter.videoViews. - Set the large video view to
doubleClickFullSession?.hostingView. - Set
containerViewas the containing view for the videos. - Update the layout using
videoViewLayouter.layoutVideoViews(). - Invoke
updateSelfViewVisiable().
Note: The backgroundDoubleTap gesture recognizer is enabled only for 3 or more video sessions. This gesture recognizer enables the ability to change the layout.
func updateInterface(with sessions: [VideoSession], targetSize: CGSize) {
guard !sessions.isEmpty else {
return
}
let selfSession = sessions.first!
videoViewLayouter.selfView = selfSession.hostingView
videoViewLayouter.selfSize = selfSession.size
videoViewLayouter.targetSize = targetSize
var peerVideoViews = [VideoView]()
for i in 1..<sessions.count {
peerVideoViews.append(sessions[i].hostingView)
}
videoViewLayouter.videoViews = peerVideoViews
videoViewLayouter.fullView = doubleClickFullSession?.hostingView
videoViewLayouter.containerView = containerView
videoViewLayouter.layoutVideoViews()
updateSelfViewVisiable()
//Only three people or more can switch the layout
if sessions.count >= 3 {
backgroundDoubleTap.isEnabled = true
} else {
backgroundDoubleTap.isEnabled = false
doubleClickFullSession = nil
}
}The setIdleTimerActive() method updates the idle timer of the sample application to be either active or inactive.
func setIdleTimerActive(_ active: Bool) {
UIApplication.shared.isIdleTimerDisabled = !active
}The fetchSession() method returns the VideoSession for a specified user. Loop through videoSessions until the session.uid matches the uid.
func fetchSession(of uid: UInt) -> VideoSession? {
for session in videoSessions {
if session.uid == uid {
return session
}
}
return nil
}The videoSession() method returns the VideoSession for the user. The difference between this method and the fetchSession() method is that if no fetchSession() exists a new VideoSession object is created and appended to videoSessions.
func videoSession(of uid: UInt) -> VideoSession {
if let fetchedSession = fetchSession(of: uid) {
return fetchedSession
} else {
let newSession = VideoSession(uid: uid)
videoSessions.append(newSession)
return newSession
}
}The setVideoMuted() method starts/stops the video for a specified user. The VideoSession is retrieved using fetchSession() to apply muted to the isVideoMuted property.
func setVideoMuted(_ muted: Bool, forUid uid: UInt) {
fetchSession(of: uid)?.isVideoMuted = muted
}The updateSelfViewVisiable() method sets the user view to hidden/not hidden. If the number of videoSessions is 2, determine if the view is hidden using videoMuted.
func updateSelfViewVisiable() {
guard let selfView = videoSessions.first?.hostingView else {
return
}
if videoSessions.count == 2 {
selfView.isHidden = videoMuted
} else {
selfView.isHidden = false
}
}The alert() method appends an alert message to the chat message box using chatMessageVC?.append().
func alert(string: String) {
guard !string.isEmpty else {
return
}
chatMessageVC?.append(alert: string)
}The methods applying the Agora SDK are placed within a private extension for the RoomViewController.
//MARK: - engine
private extension RoomViewController {
...
}- Create the loadAgoraKit() Method
- Create the addLocalSession() Method
- Create the leaveChannel() Method
- Create the send() Method
- Create the AgoraRtcEngineDelegate
The loadAgoraKit() method initializes the Agora RTC engine using AgoraRtcEngineKit.sharedEngine():
-
Set the channel profile to
.communication, enable video, and set thevideoProfile. -
Invoke
addLocalSession()and start the preview usingagoraKit.startPreview(). -
If
encryptionSecretis not empty, set the encryption usingagoraKit.setEncryptionMode()andagoraKit.setEncryptionSecret(). -
Join the channel
roomNameusingagoraKit.joinChannel():
- If the
codeis equal to0, the channel join is successful. Disable the idle timer usingsetIdleTimerActive. - If the channel join is not successful, display an error message alert using
self.alert().
- Complete the method with
agoraKit.createDataStream()to create a data stream for the joined channel.
func loadAgoraKit() {
agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: self)
agoraKit.setChannelProfile(.communication)
agoraKit.enableVideo()
agoraKit.setVideoProfile(videoProfile, swapWidthAndHeight: false)
addLocalSession()
agoraKit.startPreview()
if let encryptionType = encryptionType, let encryptionSecret = encryptionSecret, !encryptionSecret.isEmpty {
agoraKit.setEncryptionMode(encryptionType.modeString())
agoraKit.setEncryptionSecret(encryptionSecret)
}
let code = agoraKit.joinChannel(byToken: nil, channelId: roomName, info: nil, uid: 0, joinSuccess: nil)
if code == 0 {
setIdleTimerActive(false)
} else {
DispatchQueue.main.async(execute: {
self.alert(string: "Join channel failed: \(code)")
})
}
agoraKit.createDataStream(&dataChannelId, reliable: true, ordered: true)
}The addLocalSession() method appends the local video session to videoSessions and sets up the local video view using agoraKit.setupLocalVideo().
If MediaInfo is available for the videoProfile, set the media info property for the local session using localSession.mediaInfo.
func addLocalSession() {
let localSession = VideoSession.localSession()
videoSessions.append(localSession)
agoraKit.setupLocalVideo(localSession.canvas)
if let mediaInfo = MediaInfo(videoProfile: videoProfile) {
localSession.mediaInfo = mediaInfo
}
}The leaveChannel() method enables the user to leave the video session.
- Clear the local video and leave the channel by applying
nilas the parameter foragoraKit.setupLocalVideo()andagoraKit.leaveChannel(). - Stop the video preview using
agoraKit.stopPreview()and setisFilteringtofalse. - Loop through
videoSessionsand remove itshostingViewfrom the superview usingremoveFromSuperview(). - Clear the video sessions array using
videoSessions.removeAll(). - Set the idle timer to active using
setIdleTimerActive(). - Complete the method by invoking the room to close using
delegate?.roomVCNeedClose().
func leaveChannel() {
agoraKit.setupLocalVideo(nil)
agoraKit.leaveChannel(nil)
agoraKit.stopPreview()
isFiltering = false
for session in videoSessions {
session.hostingView.removeFromSuperview()
}
videoSessions.removeAll()
setIdleTimerActive(true)
delegate?.roomVCNeedClose(self)
}The send() method sends a new message to the stream using agoraKit.sendStreamMessage().
Append the message to the chat message view using chatMessageVC?.append().
func send(text: String) {
if dataChannelId > 0, let data = text.data(using: String.Encoding.utf8) {
agoraKit.sendStreamMessage(dataChannelId, data: data)
chatMessageVC?.append(chat: text, fromUid: 0)
}
}The AgoraRtcEngineDelegate methods are added through an extension for the RoomViewController.
//MARK: - engine delegate
extension RoomViewController: AgoraRtcEngineDelegate {
...
}- Create the rtcEngine Connection Methods
- Create the errorCode Event Listener
- Create the firstRemoteVideoDecodedOfUid Event Listener
- Create the firstLocalVideoFrameWith Event Listener
- Create the didOfflineOfUid Event Listener
- Create the didVideoMuted Event Listener
- Create the remoteVideoStats Event Listener
- Create the receiveStreamMessageFromUid Event Listener
- Create the didOccurStreamMessageErrorFromUid Event Listener
The rtcEngineConnectionDidInterrupted() method displays an alert with the error message Connection Interrupted.
The rtcEngineConnectionDidLost() method displays an alert with the error message Connection Lost.
func rtcEngineConnectionDidInterrupted(_ engine: AgoraRtcEngineKit) {
alert(string: "Connection Interrupted")
}
func rtcEngineConnectionDidLost(_ engine: AgoraRtcEngineKit) {
alert(string: "Connection Lost")
}The didOccurError event listener is triggered when the Agora RTC engine generates an error. Use this for logging and debugging.
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
//
}The firstRemoteVideoDecodedOfUid event listener is triggered when the first remote video is decoded.
-
Retrieve the video session of the user using
videoSession(). -
Set the session dimensions using
userSession.sizeand update the media info usinguserSession.updateMediaInfo(). -
Complete the method by setting up the remote video using
agoraKit.setupRemoteVideo().
func rtcEngine(_ engine: AgoraRtcEngineKit, firstRemoteVideoDecodedOfUid uid: UInt, size: CGSize, elapsed: Int) {
let userSession = videoSession(of: uid)
let sie = size.fixedSize(with: containerView.bounds.size)
userSession.size = sie
userSession.updateMediaInfo(resolution: size)
agoraKit.setupRemoteVideo(userSession.canvas)
}The firstLocalVideoFrameWith event listener is triggered when the first local video frame has elapsed.
Set the dimensions of the video session using selfSession.size and update the video interface using updateInterface().
//first local video frame
func rtcEngine(_ engine: AgoraRtcEngineKit, firstLocalVideoFrameWith size: CGSize, elapsed: Int) {
if let selfSession = videoSessions.first {
let fixedSize = size.fixedSize(with: containerView.bounds.size)
selfSession.size = fixedSize
updateInterface(with: videoSessions, targetSize: containerView.frame.size, animation: false)
}
}The didOfflineOfUid is triggered when a user goes offline.
Loop through videoSessions to retrieve the video session of the offline user:
- If the video session is found, remove the session
hostingViewfrom the superview usingremoveFromSuperview(). - If the offline user session is
doubleClickFullSession, setdoubleClickFullSessiontonil.
//user offline
func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
var indexToDelete: Int?
for (index, session) in videoSessions.enumerated() {
if session.uid == uid {
indexToDelete = index
}
}
if let indexToDelete = indexToDelete {
let deletedSession = videoSessions.remove(at: indexToDelete)
deletedSession.hostingView.removeFromSuperview()
if let doubleClickFullSession = doubleClickFullSession , doubleClickFullSession == deletedSession {
self.doubleClickFullSession = nil
}
}
}The didVideoMuted is triggered when a user turns off video.
Set the video to off using setVideoMuted().
//video muted
func rtcEngine(_ engine: AgoraRtcEngineKit, didVideoMuted muted: Bool, byUid uid: UInt) {
setVideoMuted(muted, forUid: uid)
}The remoteVideoStats event is triggered when a metric changes for the Agora RTC engine.
Retrieve the video session for the user using fetchSession() and update the resolution, height, and fps using session.updateMediaInfo().
//remote stat
func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStats stats: AgoraRtcRemoteVideoStats) {
if let session = fetchSession(of: stats.uid) {
session.updateMediaInfo(resolution: CGSize(width: CGFloat(stats.width), height: CGFloat(stats.height)), fps: Int(stats.receivedFrameRate))
}
}The receiveStreamMessageFromUid is triggered when a message is received from a user.
The method checks that the message string is not empty before appending it to the chat message view using chatMessageVC?.append().
//data channel
func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) {
guard let string = String(data: data, encoding: String.Encoding.utf8) , !string.isEmpty else {
return
}
chatMessageVC?.append(chat: string, fromUid: Int64(uid))
}The didOccurStreamMessageErrorFromUid is triggered when a user message error occurs and then logs the error using print().
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurStreamMessageErrorFromUid uid: UInt, streamId: Int, error: Int, missed: Int, cached: Int) {
print("Data channel error: \(error), missed: \(missed), cached: \(cached)\n")
}ChatMessageViewController.swift defines and connects application functionality with the ChatMessageViewController UI.
- Add Global Variables and Superclass Overrides
- Create append() Methods
- Create the UITableViewDataSource Object
The ChatMessageViewController defines the IBOutlet variable messageTableView, which maps to the table created in the ChatMessageViewController UI.
-
Initialize the private variable
messageListto manage the array of messages for the chat. -
When the
viewDidLoad()method is invoked, set the row height for the message table usingmessageTableView.rowHeightand set theestimatedRowHeightto24.
import UIKit
class ChatMessageViewController: UIViewController {
@IBOutlet weak var messageTableView: UITableView!
fileprivate var messageList = [Message]()
override func viewDidLoad() {
super.viewDidLoad()
messageTableView.rowHeight = UITableViewAutomaticDimension
messageTableView.estimatedRowHeight = 24
}
...
}The append() methods are used to add messages and alerts to the message window.
-
The
append()method for achatcreates a newMessageobject of type.chatand invokes theappend()method for themessage. -
The
append()method for analertcreates a newMessageobject of type.alertand invokes theappend()method formessage.
func append(chat text: String, fromUid uid: Int64) {
let message = Message(text: text, type: .chat)
append(message: message)
}
func append(alert text: String) {
let message = Message(text: text, type: .alert)
append(message: message)
}The append() method for a message is created in an extension for the ChatMessageViewController.
The message is added to the messageList.
When the messageList contains more than 20 messages, delete the first message in the array using updateMessageTable().
private extension ChatMessageViewController {
func append(message: Message) {
messageList.append(message)
var deleted: Message?
if messageList.count > 20 {
deleted = messageList.removeFirst()
}
updateMessageTable(with: deleted)
}
...
}The updateMessageTable() method is a helper method to handle messages for the chat view.
-
Check that the
messageTableViewexists. If it does not exist, stop the method usingreturn. -
Retrieve the
IndexPathfor the last message by usingmessageList.count - 1. -
Invoke
tableView.beginUpdates()and delete any necessary rows usingtableView.deleteRows(). -
Add the new message to the table using
tableView.insertRows()and complete the table updates usingtableView.endUpdates(). -
Display the last message on the screen using
tableView.scrollToRow().
func updateMessageTable(with deleted: Message?) {
guard let tableView = messageTableView else {
return
}
let insertIndexPath = IndexPath(row: messageList.count - 1, section: 0)
tableView.beginUpdates()
if deleted != nil {
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .none)
}
tableView.insertRows(at: [insertIndexPath], with: .none)
tableView.endUpdates()
tableView.scrollToRow(at: insertIndexPath, at: .bottom, animated: false)
}The tableView() data source methods are defined in an extension to the ChatMessageViewController.
- Return a
messageList.countas the number of rows in the table section.
extension ChatMessageViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messageList.count
}
...
}-
Create the table cell using
tableView.dequeueReusableCell(). -
Set the cell
messageusingcell.setand return the resulting cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "messageCell", for: indexPath) as! ChatMessageCell
let message = messageList[(indexPath as NSIndexPath).row]
cell.set(with: message)
return cell
}SettingsViewController.swift defines and connects application functionality with the SettingsViewController UI.
- Create Variables, Protocols, and IBAction Methods
- Create Delegate and DataSource Methods
- Create Agora Methods
The settingsVC() protocol method is used by external classes to update the video profile.
import UIKit
protocol SettingsVCDelegate: NSObjectProtocol {
func settingsVC(_ settingsVC: SettingsViewController, didSelectProfile profile: AgoraVideoProfile)
}The profileTableView IBOutlet variable maps to the profile table created in the SettingsViewController UI.
class SettingsViewController: UIViewController {
@IBOutlet weak var profileTableView: UITableView!
...
}When the videoProfile is set, profileTableView?.reloadData() is invoked to update the profile table information.
var videoProfile: AgoraVideoProfile! {
didSet {
profileTableView?.reloadData()
}
}The delegate variable is an optional SettingsVCDelegate object.
The private profiles variable is an array of AgoraVideoProfile objects and is initialized with AgoraVideoProfile.list().
weak var delegate: SettingsVCDelegate?
fileprivate let profiles: [AgoraVideoProfile] = AgoraVideoProfile.list()The doConfirmPressed() IBAction method is invoked by the OK button in the UI layout. This method updates the video profile by invoking delegate?.settingsVC().
@IBAction func doConfirmPressed(_ sender: UIButton) {
delegate?.settingsVC(self, didSelectProfile: videoProfile)
}The tableView() data source methods are added in an extension to the SettingsViewController.
- Return the
profiles.countas the number of rows in the table section.
extension SettingsViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return profiles.count
}
...
}-
Create the table cell using
tableView.dequeueReusableCell(). -
Set the cell's
selectedProfileusingcell.updateand return the resulting cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "profileCell", for: indexPath) as! ProfileCell
let selectedProfile = profiles[indexPath.row]
cell.update(with: selectedProfile, isSelected: (selectedProfile == videoProfile))
return cell
}The tableView() delegate method is added in an extension to the SettingsViewController.
When a table row is selected, set videoProfile to the selectedProfile.
extension SettingsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedProfile = profiles[indexPath.row]
videoProfile = selectedProfile
}
}The AgoraVideoProfile extension adds a list() method, which returns an array of AgoraVideoProfile objects using AgoraVideoProfile.validProfileList().
private extension AgoraVideoProfile {
static func list() -> [AgoraVideoProfile] {
return AgoraVideoProfile.validProfileList()
}
}






