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

../_images/ios_quickstart_1.png

Open the Carthage and then the iOS folder in order to drag all .framework files and drop them into Xcode:

../_images/ios_quickstart_3.png

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>
../_images/ios_quickstart_6.png

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()