- 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.framework
libresolv.tbd
AgoraRtcEngineKit.framework
CoreTelephony.framework
CoreMedia.framework
VideoToolbox.framework
AudioToolbox.framework
libc++.tbd
AgoraRtcCryptoLoader.framework
AVFoundation.framework
libcrypto.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.videoProfile
to the currentvideoProfile
. - Set
settingsVC.delegate
toself
.
If the segueId
is mainToRoom
, prepare the room view through the segue destination RoomViewController
:
- Set
roomVC.roomName
tosender
. - Set
roomVC.encryptionSecret
to the text entered in theencryptionTextField
. - Set
roomVC.encryptionType
to the currentencryptionType
. - Set
roomVC.videoProfile
to the currentvideoProfile
. - Set
roomVC.delegate
toself
.
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
UIAlertAction
object for each encryption type and add it tosheet
. -
Create a
cancel
UIAlertAction
object and add it tosheet
. -
Apply the popover above
encryptionButton
by settingsheet.popoverPresentationController?.sourceView
and settingsheet.popoverPresentationController?.permittedArrowDirections
to.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
muteVideoButton
image is updated. - The
cameraButton
andspeakerButton
are 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
agoraKit
is verified. - If filtering is enabled, set the video preprocessing using
AGVideoPreProcessing.registerVideoPreprocessing()
and update thefilterButton
with the blue image. - If filtering is not enabled, update the
filterButton
with 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
messageTextField
is activated/deactivated usingbecomeFirstResponder()
/resignFirstResponder()
. - the
messageInputerView
is hidden/unhidden. - the
messageButton
image 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
roomNameLabel
text. - Set the
backgroundTap
gesture 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
doubleClickFullSession
isnil
, detect the video session index, and setdoubleClickFullSession
to the selected video session. - If
doubleClickFullSession
already exists, setdoubleClickFullSession
to 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
duration
is greater than0
, animatestrongSelf.messageInputerBottom
using the animation typeUIKeyboardAnimationCurveUserInfoKey
if available. -
If the
duration
is less than or equal to0
, setstrongSelf.messageInputerBottom.constant
todeltaY
.
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
userInfo
from the notification'suserInfo
and extractdurationValue
/duration
. -
Return the message box to the bottom of the screen:
-
If the
duration
is greater than0
, animatestrongSelf.messageInputerBottom
using the animation typeUIKeyboardAnimationCurveUserInfoKey
if available. -
If the
duration
is less than or equal to0
, setstrongSelf.messageInputerBottom.constant
to0
.
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 withanimation
checks 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 forvideoViewLayouter
and video views.
Loop through sessions
to retrieve each hostingView
and append the view to peerVideoViews
:
- Apply
peerVideoViews
to the video layout manager usingvideoViewLayouter.videoViews
. - Set the large video view to
doubleClickFullSession?.hostingView
. - Set
containerView
as 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
encryptionSecret
is not empty, set the encryption usingagoraKit.setEncryptionMode()
andagoraKit.setEncryptionSecret()
. -
Join the channel
roomName
usingagoraKit.joinChannel()
:
- If the
code
is 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
nil
as the parameter foragoraKit.setupLocalVideo()
andagoraKit.leaveChannel()
. - Stop the video preview using
agoraKit.stopPreview()
and setisFiltering
tofalse
. - Loop through
videoSessions
and remove itshostingView
from 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.size
and 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
hostingView
from the superview usingremoveFromSuperview()
. - If the offline user session is
doubleClickFullSession
, setdoubleClickFullSession
tonil
.
//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
messageList
to manage the array of messages for the chat. -
When the
viewDidLoad()
method is invoked, set the row height for the message table usingmessageTableView.rowHeight
and set theestimatedRowHeight
to24
.
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 achat
creates a newMessage
object of type.chat
and invokes theappend()
method for themessage
. -
The
append()
method for analert
creates a newMessage
object of type.alert
and 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
messageTableView
exists. If it does not exist, stop the method usingreturn
. -
Retrieve the
IndexPath
for 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.count
as 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
message
usingcell.set
and 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.count
as 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
selectedProfile
usingcell.update
and 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()
}
}