iOS tutorial
Follow this tutorial to run an example project developed with Quobis iOS SDK.
Project prerequisites
The prerequisites to use the Quobis iOS SDK are:
Xcode 11
Swift 4.0 or higher
Support for iOS 11.0+
Project dependencies
The Quobis SDK can be downloaded from the SDK donwload page
In order to run a project with Quobis iOS SDK, you have to add the following framework dependencies to Xcode.
Install Carthage
Quobis iOS SDK uses Carthage to manage project dependencies. There are multiple options for installing Carthage but it is recommended to use Homebrew:
brew update
and
brew install carthage
Note: if you previously installed the binary version of Carthage, you should delete /Library/Frameworks/CarthageKit.framework
. In order to run an upgrade, execute:
carthage update --platform iOS
Add frameworks to Xcode project
Put the Carthage folder in the project root
Open the Carthage and then the iOS folder in order to drag all .framework files and drop them into Xcode:
Next, switch to Build Phases. Click the + icon in the top-left of the editor and select New Run Script Phase. Add the following command to the block of code under Run Script: /usr/local/bin/carthage copy-frameworks
Click the + icon under Input Files and add an entry for each framework:
(SRCROOT)/Carthage/Build/iOS/SippoClient.framework
$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework
Configure AppDelegate
In your __AppDelegate__ or in any place you consider, you can configure your Sippo client, for example:
1 func application(_ application: UIApplication,
2 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
3 SippoClient.configure(with: SippoClient.Options(sippoServerUrl: "serverUrl", xmppHost: "xmppHostUrl"))
4 return true
5 }
Session
To log in
1 SippoClient.client.sessions.connect(authentication: .basic(username: "userName", password: "yourpassword")) { result in
2 switch result {
3 case .success:
4 // on-line
5 case .failure(let error):
6 // error
7 debugPrint(error)
8 }
9 }
To log out
1SippoClient.client.sessions.disconnect { _ in
2 switch result {
3 case .success(_):
4 // offline
5 case .failure(let error):
6 debugPrint(error)
7 }
8 }
Presence
We can obtain the status of a user using the “presences”: Away, Busy, Offline and Online. To obtain the presence it is necessary to be logged in.
To obtain the presence:
1 private var presence: Presences?
2
3 presence = try? SippoClient.client.presences().get()
4
5 if let presence = presence {
6 presence.ownPresence(result: { result in
7 switch result {
8 case let .success(presence):
9 // Obtenida presence
10 print("Status: \(presence.status)")
11 case let .failure(error):
12 print("Failure get presences: \(error)")
13 }
14 })
15}
To change the presence:
We get the presence with the following code:
1private var presence: Presences?
2private var presenceUser: Presence?
3
4presence = try? SippoClient.client.presences().get()
5
6if let presence = presence {
7 presence.ownPresence(result: { result in
8 switch result {
9 case let .success(presence):
10 // Obtenida presence
11 self.presenceUser = presence
12 print("Status: \(presence.status)")
13 case let .failure(error):
14 print("Failure get presences: \(error)")
15 }
16 })
17}
We update the presence with a new status, you can put the following statuses: .away, .busy, .offline, and .online
1if let presence = presence,
2 let presenceUser = presenceUser {
3
4 let newPresence = Presence(status: .away,
5 avatar: presenceUser.avatar,
6 displayName: presenceUser.displayName,
7 address: presenceUser.address)
8
9 presence.updateMyPresence(newPresence) { result in
10 switch result {
11 case let .success(presence):
12 // Update presence
13 print("Status: \(presence.status)")
14 case let .failure(error):
15 print("Failure joining the conference: \(error)")
16 }
17 }
18}
To display the user’s avatar:
1private var presence: Presences?
2presence = try? SippoClient.client.presences().get()
3
4if let presence = presence {
5 presence.ownPresence(result: { result in
6 switch result {
7
8 case let .success(presence):
9
10 print("Status: \(presence.status)")
11 if let avatar = presence.avatar {
12 let avatarSanatized = avatar.replacingOccurrences(of: "data:image/png;base64,", with: "")
13 let imageData = Data(base64Encoded: avatarSanatized, options: .ignoreUnknownCharacters)
14
15 if let imageData = imageData {
16 DispatchQueue.main.async {
17 self.userImageView.image = UIImage(data: imageData)
18 }
19 }
20 }
21 case let .failure(error):
22 print("Failure getting presences: \(error)")
23 }
24 })
25}
Chat
Chat object:
1public class Chat {
2
3 public enum State {
4
5 case joined
6
7 case left
8 }
9
10 /// Define wether a chat is a group or not
11 public enum ChatType {
12
13 case oneToOne
14
15 case groupChat
16 }
17
18 /// Delegate
19 weak public var delegate: SippoClient.ChatDelegate?
20
21 /// Expose chat type (1-1 or group)
22 public var type: SippoClient.Chat.ChatType { get }
23
24 /// Chat identifier (remote user for 1-1 and UUID for group chats)
25 public var identifier: String { get }
26
27 /// Group actions for group chats like invite a participant or expel one
28 public private(set) var groupChatActions: SippoClient.GroupChatActions? { get }
29
30 /// Last message received
31 public fileprivate(set) var lastMessage: SippoClient.ChatMessage?
32
33 /// Number of unread messages, should be updated calling `Chat.fetchUnreadMessagesCount()`
34 public fileprivate(set) var unreadMessagesCount: Int
35
36 /// Leaves the chat
37 public func leave(completion: @escaping () -> Void)
38
39 /// Sends a new message to other participant
40 /// - Parameters:
41 /// - message: Body message to be sent
42 /// - Returns: A Message
43 public func send(message: String) -> SippoClient.ChatMessage
44
45 /// Send participant action, like typing or active, to remote participant
46 /// - Parameter action: Current participant action
47 public func sendParticipantActionInChat(_ action: SippoClient.ChatMessage.ParticipantAction)
48
49 /// Send a file to Chat
50 /// - Parameters:
51 /// - file: Data to send
52 /// - filename: Name use to share it
53 /// - Returns: A Message where upload progress is received
54 public func send(file: Data, named filename: String, pathToLocalFile: String?) -> SippoClient.ChatMessage
55
56 /// Mark a message with a new status
57 ///
58 /// - Parameters:
59 /// - message: Message to change status
60 /// - status: New status
61 public func mark(message: SippoClient.ChatMessage, as status: SippoClient.ChatMessage.Status)
62
63 /// Fetch history related to this user, messages are received in delegate
64 ///
65 /// - Parameters:
66 /// - messageSearch: search criteria
67 /// - completion: completion block with `ChatMessageSearchResult`
68 public func fetchMessagesFromStore(messageSearch: SippoClient.ChatMessageSearch, completion: @escaping (SippoClient.ChatMessageSearchResult) -> ())
69
70 /// Send a mark with last message read
71 public func markChatAsRead()
72
73 public func archive(completion: @escaping () -> Void)
74 }
Retrieve “1 to 1” chat list:
1private var chats: Chats?
2private var chatList = [Chat]()
3
4chats = try? SippoClient.client.chats().get()
5
6if let chats = chats {
7 chats.connect {
8 chats.chats(types: [.oneToOne]) { result in
9 switch result {
10 case let .success(chat):
11 self.chatList = chat
12 case let .failure(error):
13 print("Failure get chats: \(error)")
14 }
15 }
16 }
17}
Returns an array of Chat objects.
Restore chat history
1private var chats: Chats?
2private var chat: Result<Chat, ResourceError>?
3private var messages = [ChatMessage]()
4
5chats = try? SippoClient.client.chats().get()
6
7if let chats = chats {
8 chats.add(delegate: self)
9 chats.reconnectSession {
10 chats.createChat(creationType: .oneToOne("user@email.com")) { result in
11 switch result {
12 case .success(_):
13 self.chat = result
14 do {
15 try result.get().delegate = self
16 } catch {
17 debugPrint("error")
18 }
19
20 if let chat = self.chat {
21 do {
22 try chat.get().fetchMessagesFromStore(messageSearch: ChatMessageSearch(limitTo: 100,
23 cursor: .lastPage,
24 startDate: nil,
25 endDate: nil)) { result in
26 self.messages = result.messages
27 }
28 } catch {
29 debugPrint("error")
30 }
31 }
32 case let .failure(error):
33 debugPrint("Error creating the chat: \(error)")
34 }
35 }
36 }
37 }
Send a message in individual chat:
1private var chats: Chats?
2private var chat: Result<Chat, ResourceError>?
3private var resultSendMessage: ChatMessage?
4
5chats = try? SippoClient.client.chats().get()
6
7if let chats = chats {
8 chats.add(delegate: self)
9 chats.reconnectSession {
10 chats.createChat(creationType: .oneToOne("user@email.com")) { result in
11 switch result {
12 case .success(_):
13 self.chat = result
14
15 if let chat = chat {
16 do {
17 if true {
18 // send picture message
19 if let image = UIImage(named: "myImage"),
20 let imageData = image.pngData() {
21 self.resultSendMessage = try chat.get().send(file: imageData,
22 named: "photo.png", pathToLocalFile: nil)
23 }
24 } else {
25 // send message without picture
26 self.resultSendMessage = try chat.get().send(message: message)
27 }
28 } catch {
29 debugPrint(error)
30 }
31 }
32 case let .failure(error):
33 debugPrint("Error creating the chat: \(error)")
34 }
35 }
36 }
37 }
Receive message in individual chat:
1private var chats: Chats?
2private var chat: Result<Chat, ResourceError>?
3private var messages = [ChatMessage]()
4
5
6chats = try? SippoClient.client.chats().get()
7
8if let chats = chats {
9 chats.add(delegate: self)
10 chats.reconnectSession {
11 chats.createChat(creationType: .oneToOne("user@email.com")) { result in
12 switch result {
13 case .success(_):
14 self.chat = result
15 do {
16 try result.get().delegate = self
17 } catch {
18 debugPrint("error")
19 }
20 case let .failure(error):
21 debugPrint("Error creating the chat: \(error)")
22 }
23 }
24 }
25 }
26
27
28extension MyViewController: ChatDelegate {
29 func chat(_ chat: Chat, didReceiveMessage message: ChatMessage) {
30 // We received a message
31 messages.append(message)
32 }
33
34 func chat(_ chat: Chat, didReceiveMessageStatusUpdate message: ChatStatusMessage) {
35 }
36
37 func chat(_ chat: Chat, didReceiveParticipantAction: ChatMessage.ParticipantAction, from participant: ChatParticipant) {
38 }
39
40 func chat(_ chat: Chat, didUpdateLastMessage: ChatMessage) {
41 }
42
43 func chat(_ chat: Chat, didUpdateUnreadMessagesCount: ChatMessage) {
44 }
45
46}
Group Chat
Retrieve a list of group chats:
1private var chats: Chats?
2private var chatList = [Chat]()
3
4chats = try? SippoClient.client.chats().get()
5
6 if let chats = chats {
7 chats.chats(types: [.groupChat]) { result in
8 switch result {
9 case let .success(chat):
10 self.chatList = chat
11 DispatchQueue.main.sync {
12 self.groupChatListTableView.reloadData()
13 }
14 case let .failure(error):
15 print("Failure getting presences: \(error)")
16 }
17 }
18 }
Returns an array of Chat objects.
Create a group chat:
1private var chats: Chats?
2private var chat: Result<Chat, ResourceError>?
3
4chats = try? SippoClient.client.chats().get()
5
6if let chats = chats {
7 chats.add(delegate: self)
8 chats.reconnectSession {
9 chats.createChat(creationType: .groupChat(name: "Super Group", participants: ["user@email.com", "user2@email.com"])) { result in
10 switch result {
11 case .success(_):
12 self.chat = result
13 do {
14 try result.get().delegate = self
15 } catch {
16 debugPrint("error")
17 }
18 case let .failure(error):
19 debugPrint("Error creating the chat: \(error)")
20 }
21 }
22 }
23 }
Send/receive group chat message:
It is the same as in Chat, check the Chat section.
Conference
It is important to set the following privacy keys in the info.plist file of the project in order to get permissions to access microphone and camera.
1 <key>NSCameraUsageDescription</key>
2 <string>$(PRODUCT_NAME) Needed camera permissions to work</string>
3 <key>NSMicrophoneUsageDescription</key>
4 <string>$(PRODUCT_NAME) Needed microphone permissions to work</string>
Make a call:
1private var conferences: Conferences?
2private var conference: SippoConference?
3
4override func viewDidLoad() {
5 super.viewDidLoad()
6
7 conferences = try? SippoClient.client.conferences().get()
8}
We create a conference and we join:
1if let conferences = conferences {
2 conferences.createConference(recording: false,
3 delegate: self) { result in
4 switch result {
5 case let .success(conference):
6 debugPrint("New conference: \(conference)")
7
8 self.conference = conference
9
10 self.conference?.join(for: [.video, .audio],
11 // [.video, .audio] or [.audio]
12 configuration: ConferenceConfiguration()) { result in
13 switch result {
14 case .success:
15 debugPrint("creating conference")
16 case let .failure(error):
17 debugPrint("Failure joining to conference: \(error)")
18 }
19 }
20 case let .failure(error):
21 debugPrint("Failure creating conference: \(error)")
22 }
23 }
24}
We invite and call an user
1conference?.invite(to: "user@email.com",
2 for: [.video, .audio],
3 replyOf: .none,
4 result: { result in
5
6 switch result {
7 case let .success(response):
8 debugPrint("response: \(response)")
9 case let .failure(error):
10 debugPrint("error: \(error)")
11 }
12 })
We show the video that we get
1extension CallViewController: SippoConferenceDelegate {
2 func conferenceUpdated(_ conference: SippoConference) {
3 debugPrint("### 1")
4 debugPrint("\(conference)")
5 }
6
7 func conferenceDestroyed() {
8 debugPrint("### 2")
9 }
10
11 func conference(_ conference: SippoConference, didCancelInvitation: String) {
12 debugPrint("### 3")
13 debugPrint("\(conference)")
14 debugPrint("\(didCancelInvitation)")
15 }
16
17 func conference(_ conference: SippoConference, didAddParticpant: Participant) {
18 debugPrint("### 4")
19 debugPrint("\(conference)")
20 debugPrint("\(didAddParticpant)")
21 }
22
23 func conference(_ conference: SippoConference, didRemoveParticpant: Participant) {
24 debugPrint("### 5")
25 debugPrint("\(conference)")
26 debugPrint("\(didRemoveParticpant)")
27 }
28
29 func conferenceDidAddLocalMedia(_ conference: SippoConference) {
30 debugPrint("### 6")
31 debugPrint("\(conference)")
32 }
33
34 func conference(_ conference: SippoConference, didAddVideoFromParticipant participant: Participant) {
35 debugPrint("### 7")
36 debugPrint("\(conference)")
37 debugPrint("\(participant)")
38
39 let remoteVideo = conference.viewFor(remoteParticipant: participant,
40 frame: CGRect(x: 0,
41 y: 0,
42 width: 300,
43 height: 300),
44 metalRender: false)
45 if let remoteVideo = remoteVideo {
46 self.videoContainerView.addSubview(remoteVideo)
47 }
48 }
49
50 func conference(_ conference: SippoConference, didRemoveVideoFromParticipant participant: Participant) {
51 debugPrint("### 8") // a user has left the conference
52 debugPrint("\(conference)")
53 debugPrint("\(participant)")
54 }
55
56 func conference(_ conference: SippoConference, didReceiveUnattendedTransfer: SippoTransfer) {
57 debugPrint("### 9")
58 debugPrint("\(conference)")
59 debugPrint("\(didReceiveUnattendedTransfer)")
60 }
61 }
Receive a call:
1private var conferences: Conferences?
2private var conference: SippoConference?
3
4override func viewDidLoad() {
5 super.viewDidLoad()
6
7 conferences = try? SippoClient.client.conferences().get()
8 conferences?.add(delegate: self)
9}
Enter the conference by the delegate method and we show the video on the screen.
1extension MyViewController: ConferencesDelegate {
2 func conferences(_ conferences: Conferences, didReceiveNewConference: SippoConference, from caller: Participant, using constraints: [ConferenceConstraint]) {
3
4 // You are receiving a new call
5
6 self.conference = didReceiveNewConference
7
8 let alertController = UIAlertController(title: "Conference",
9 message: "You are receiving a new call",
10 preferredStyle: .alert)
11
12 let alertAction = UIAlertAction(title: "Accept",
13 style: .default) { alertAction in
14 self.conference?.join(for: constraints,
15 configuration: ConferenceConfiguration()) { result in
16 switch result {
17 case .success:
18 DispatchQueue.main.async {
19 let remoteVideo = self.conference?.viewFor(remoteParticipant: caller,
20 frame: CGRect(x: 0,
21 y: 0,
22 width: 300,
23 height: 300),
24 metalRender: false)
25 if let remoteVideo = remoteVideo {
26 self.videoContainerView.addSubview(remoteVideo)
27 }
28 }
29
30 case let .failure(error):
31 debugPrint("Failure joining to conference: \(error)")
32 }
33 }
34 }
35
36 let cancelAction = UIAlertAction(title: "Cancel",
37 style: .cancel) { alertAction in
38 // we reject the call
39 self.conference?.declineInvitation()
40 }
41
42 alertController.addAction(alertAction)
43 alertController.addAction(cancelAction)
44
45 DispatchQueue.main.async {
46 self.present(alertController, animated: true, completion: nil)
47 }
48 }
49 }
Reject a call
1private var conference: SippoConference?
2self.conference?.declineInvitation()