Skip to contentSkip to navigationSkip to topbar
On this page

Chat with iOS and Swift


(error)

Danger

Programmable Chat has been deprecated and is no longer supported. Instead, we'll be focusing on the next generation of chat: Twilio Conversations. Find out more about the EOL process here(link takes you to an external page).

If you're starting a new project, please visit the Conversations Docs to begin. If you've already built on Programmable Chat, please visit our Migration Guide to learn about how to switch.

(warning)

Warning

As the Programmable Chat API is set to sunset in 2022(link takes you to an external page), we will no longer maintain these chat tutorials.

Please see our Conversations API QuickStart to start building robust virtual spaces for conversation.

Ready to implement a chat application using Twilio Chat Client? Here is how it works at a high level:

  1. Programmable Chat is the core product we'll be using to handle all the chat functionality.
  2. We use a server side app to generate user access tokens which contains all your Twilio account information. The Programmable Chat Client uses this token to connect with the API.

Properati built a web and mobile messaging app to help real estate buyers and sellers connect in real time. Learn more here.(link takes you to an external page)

For your convenience, we consolidated the source code for this tutorial in a single GitHub repository(link takes you to an external page). Feel free to clone it and tweak as required.


Initialize the Programmable Chat Client

initialize-the-programmable-chat-client page anchor

The only thing you need to create a client is an access token. This token holds information about your Twilio account and Programmable Chat API keys. We have created a web version of Twilio chat in different languages. You can use any of these to generate the token:

(information)

Info

You will need to set up your access token URL in the Keys.plist file in the resources folder. The default is http://localhost:8000/token - you may need to change this. For instance, if you set up the Node.js version of the chat server (listed above) - this URL would be http://localhost:3000/token

Fetch Access Token

fetch-access-token page anchor

twiliochat/MessagingManager.swift

1
import UIKit
2
3
class MessagingManager: NSObject {
4
5
static let _sharedManager = MessagingManager()
6
7
var client:TwilioChatClient?
8
var delegate:ChannelManager?
9
var connected = false
10
11
var userIdentity:String {
12
return SessionManager.getUsername()
13
}
14
15
var hasIdentity: Bool {
16
return SessionManager.isLoggedIn()
17
}
18
19
override init() {
20
super.init()
21
delegate = ChannelManager.sharedManager
22
}
23
24
class func sharedManager() -> MessagingManager {
25
return _sharedManager
26
}
27
28
func presentRootViewController() {
29
if (!hasIdentity) {
30
presentViewControllerByName(viewController: "LoginViewController")
31
return
32
}
33
34
if (!connected) {
35
connectClientWithCompletion { success, error in
36
print("Delegate method will load views when sync is complete")
37
if (!success || error != nil) {
38
DispatchQueue.main.async {
39
self.presentViewControllerByName(viewController: "LoginViewController")
40
}
41
}
42
}
43
return
44
}
45
46
presentViewControllerByName(viewController: "RevealViewController")
47
}
48
49
func presentViewControllerByName(viewController: String) {
50
presentViewController(controller: storyBoardWithName(name: "Main").instantiateViewController(withIdentifier: viewController))
51
}
52
53
func presentLaunchScreen() {
54
presentViewController(controller: storyBoardWithName(name: "LaunchScreen").instantiateInitialViewController()!)
55
}
56
57
func presentViewController(controller: UIViewController) {
58
let window = UIApplication.shared.delegate!.window!!
59
window.rootViewController = controller
60
}
61
62
func storyBoardWithName(name:String) -> UIStoryboard {
63
return UIStoryboard(name:name, bundle: Bundle.main)
64
}
65
66
// MARK: User and session management
67
68
func loginWithUsername(username: String,
69
completion: @escaping (Bool, NSError?) -> Void) {
70
SessionManager.loginWithUsername(username: username)
71
connectClientWithCompletion(completion: completion)
72
}
73
74
func logout() {
75
SessionManager.logout()
76
DispatchQueue.global(qos: .userInitiated).async {
77
self.client?.shutdown()
78
self.client = nil
79
}
80
self.connected = false
81
}
82
83
// MARK: Twilio client
84
85
func loadGeneralChatRoomWithCompletion(completion:@escaping (Bool, NSError?) -> Void) {
86
ChannelManager.sharedManager.joinGeneralChatRoomWithCompletion { succeeded in
87
if succeeded {
88
completion(succeeded, nil)
89
}
90
else {
91
let error = self.errorWithDescription(description: "Could not join General channel", code: 300)
92
completion(succeeded, error)
93
}
94
}
95
}
96
97
func connectClientWithCompletion(completion: @escaping (Bool, NSError?) -> Void) {
98
if (client != nil) {
99
logout()
100
}
101
102
requestTokenWithCompletion { succeeded, token in
103
if let token = token, succeeded {
104
self.initializeClientWithToken(token: token)
105
completion(succeeded, nil)
106
}
107
else {
108
let error = self.errorWithDescription(description: "Could not get access token", code:301)
109
completion(succeeded, error)
110
}
111
}
112
}
113
114
func initializeClientWithToken(token: String) {
115
DispatchQueue.main.async {
116
UIApplication.shared.isNetworkActivityIndicatorVisible = true
117
}
118
TwilioChatClient.chatClient(withToken: token, properties: nil, delegate: self) { [weak self] result, chatClient in
119
guard (result.isSuccessful()) else { return }
120
121
UIApplication.shared.isNetworkActivityIndicatorVisible = true
122
self?.connected = true
123
self?.client = chatClient
124
}
125
}
126
127
func requestTokenWithCompletion(completion:@escaping (Bool, String?) -> Void) {
128
if let device = UIDevice.current.identifierForVendor?.uuidString {
129
TokenRequestHandler.fetchToken(params: ["device": device, "identity":SessionManager.getUsername()]) {response,error in
130
var token: String?
131
token = response["token"] as? String
132
completion(token != nil, token)
133
}
134
}
135
}
136
137
func errorWithDescription(description: String, code: Int) -> NSError {
138
let userInfo = [NSLocalizedDescriptionKey : description]
139
return NSError(domain: "app", code: code, userInfo: userInfo)
140
}
141
}
142
143
// MARK: - TwilioChatClientDelegate
144
extension MessagingManager : TwilioChatClientDelegate {
145
func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {
146
self.delegate?.chatClient(client, channelAdded: channel)
147
}
148
149
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {
150
self.delegate?.chatClient(client, channel: channel, updated: updated)
151
}
152
153
func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
154
self.delegate?.chatClient(client, channelDeleted: channel)
155
}
156
157
func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {
158
if status == TCHClientSynchronizationStatus.completed {
159
UIApplication.shared.isNetworkActivityIndicatorVisible = false
160
ChannelManager.sharedManager.channelsList = client.channelsList()
161
ChannelManager.sharedManager.populateChannelDescriptors()
162
loadGeneralChatRoomWithCompletion { success, error in
163
if success {
164
self.presentRootViewController()
165
}
166
}
167
}
168
self.delegate?.chatClient(client, synchronizationStatusUpdated: status)
169
}
170
171
func chatClientTokenWillExpire(_ client: TwilioChatClient) {
172
requestTokenWithCompletion { succeeded, token in
173
if (succeeded) {
174
client.updateToken(token!)
175
}
176
else {
177
print("Error while trying to get new access token")
178
}
179
}
180
}
181
182
func chatClientTokenExpired(_ client: TwilioChatClient) {
183
requestTokenWithCompletion { succeeded, token in
184
if (succeeded) {
185
client.updateToken(token!)
186
}
187
else {
188
print("Error while trying to get new access token")
189
}
190
}
191
}
192
}
193

Now it's time to synchronize your Twilio client.


Synchronize the Programmable Chat Client

synchronize-the-programmable-chat-client page anchor

The synchronizationStatusChanged delegate(link takes you to an external page) method will allow us to know when the client has loaded all the required information. You can change the default initialization values for the client using a TwilioChatClientProperties(link takes you to an external page) instance as the options parameter in the previews step.

We need the client to be synchronized before trying to get the channel list (next step). Otherwise, calling client.channelsList()(link takes you to an external page) will return nil.

Synchronize the Chat Client

synchronize-the-chat-client page anchor

twiliochat/MessagingManager.swift

1
import UIKit
2
3
class MessagingManager: NSObject {
4
5
static let _sharedManager = MessagingManager()
6
7
var client:TwilioChatClient?
8
var delegate:ChannelManager?
9
var connected = false
10
11
var userIdentity:String {
12
return SessionManager.getUsername()
13
}
14
15
var hasIdentity: Bool {
16
return SessionManager.isLoggedIn()
17
}
18
19
override init() {
20
super.init()
21
delegate = ChannelManager.sharedManager
22
}
23
24
class func sharedManager() -> MessagingManager {
25
return _sharedManager
26
}
27
28
func presentRootViewController() {
29
if (!hasIdentity) {
30
presentViewControllerByName(viewController: "LoginViewController")
31
return
32
}
33
34
if (!connected) {
35
connectClientWithCompletion { success, error in
36
print("Delegate method will load views when sync is complete")
37
if (!success || error != nil) {
38
DispatchQueue.main.async {
39
self.presentViewControllerByName(viewController: "LoginViewController")
40
}
41
}
42
}
43
return
44
}
45
46
presentViewControllerByName(viewController: "RevealViewController")
47
}
48
49
func presentViewControllerByName(viewController: String) {
50
presentViewController(controller: storyBoardWithName(name: "Main").instantiateViewController(withIdentifier: viewController))
51
}
52
53
func presentLaunchScreen() {
54
presentViewController(controller: storyBoardWithName(name: "LaunchScreen").instantiateInitialViewController()!)
55
}
56
57
func presentViewController(controller: UIViewController) {
58
let window = UIApplication.shared.delegate!.window!!
59
window.rootViewController = controller
60
}
61
62
func storyBoardWithName(name:String) -> UIStoryboard {
63
return UIStoryboard(name:name, bundle: Bundle.main)
64
}
65
66
// MARK: User and session management
67
68
func loginWithUsername(username: String,
69
completion: @escaping (Bool, NSError?) -> Void) {
70
SessionManager.loginWithUsername(username: username)
71
connectClientWithCompletion(completion: completion)
72
}
73
74
func logout() {
75
SessionManager.logout()
76
DispatchQueue.global(qos: .userInitiated).async {
77
self.client?.shutdown()
78
self.client = nil
79
}
80
self.connected = false
81
}
82
83
// MARK: Twilio client
84
85
func loadGeneralChatRoomWithCompletion(completion:@escaping (Bool, NSError?) -> Void) {
86
ChannelManager.sharedManager.joinGeneralChatRoomWithCompletion { succeeded in
87
if succeeded {
88
completion(succeeded, nil)
89
}
90
else {
91
let error = self.errorWithDescription(description: "Could not join General channel", code: 300)
92
completion(succeeded, error)
93
}
94
}
95
}
96
97
func connectClientWithCompletion(completion: @escaping (Bool, NSError?) -> Void) {
98
if (client != nil) {
99
logout()
100
}
101
102
requestTokenWithCompletion { succeeded, token in
103
if let token = token, succeeded {
104
self.initializeClientWithToken(token: token)
105
completion(succeeded, nil)
106
}
107
else {
108
let error = self.errorWithDescription(description: "Could not get access token", code:301)
109
completion(succeeded, error)
110
}
111
}
112
}
113
114
func initializeClientWithToken(token: String) {
115
DispatchQueue.main.async {
116
UIApplication.shared.isNetworkActivityIndicatorVisible = true
117
}
118
TwilioChatClient.chatClient(withToken: token, properties: nil, delegate: self) { [weak self] result, chatClient in
119
guard (result.isSuccessful()) else { return }
120
121
UIApplication.shared.isNetworkActivityIndicatorVisible = true
122
self?.connected = true
123
self?.client = chatClient
124
}
125
}
126
127
func requestTokenWithCompletion(completion:@escaping (Bool, String?) -> Void) {
128
if let device = UIDevice.current.identifierForVendor?.uuidString {
129
TokenRequestHandler.fetchToken(params: ["device": device, "identity":SessionManager.getUsername()]) {response,error in
130
var token: String?
131
token = response["token"] as? String
132
completion(token != nil, token)
133
}
134
}
135
}
136
137
func errorWithDescription(description: String, code: Int) -> NSError {
138
let userInfo = [NSLocalizedDescriptionKey : description]
139
return NSError(domain: "app", code: code, userInfo: userInfo)
140
}
141
}
142
143
// MARK: - TwilioChatClientDelegate
144
extension MessagingManager : TwilioChatClientDelegate {
145
func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {
146
self.delegate?.chatClient(client, channelAdded: channel)
147
}
148
149
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {
150
self.delegate?.chatClient(client, channel: channel, updated: updated)
151
}
152
153
func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
154
self.delegate?.chatClient(client, channelDeleted: channel)
155
}
156
157
func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {
158
if status == TCHClientSynchronizationStatus.completed {
159
UIApplication.shared.isNetworkActivityIndicatorVisible = false
160
ChannelManager.sharedManager.channelsList = client.channelsList()
161
ChannelManager.sharedManager.populateChannelDescriptors()
162
loadGeneralChatRoomWithCompletion { success, error in
163
if success {
164
self.presentRootViewController()
165
}
166
}
167
}
168
self.delegate?.chatClient(client, synchronizationStatusUpdated: status)
169
}
170
171
func chatClientTokenWillExpire(_ client: TwilioChatClient) {
172
requestTokenWithCompletion { succeeded, token in
173
if (succeeded) {
174
client.updateToken(token!)
175
}
176
else {
177
print("Error while trying to get new access token")
178
}
179
}
180
}
181
182
func chatClientTokenExpired(_ client: TwilioChatClient) {
183
requestTokenWithCompletion { succeeded, token in
184
if (succeeded) {
185
client.updateToken(token!)
186
}
187
else {
188
print("Error while trying to get new access token")
189
}
190
}
191
}
192
}
193

We've initialized the Programmable Chat Client, now let's get a list of channels.


Get the Channel Descriptor List

get-the-channel-descriptor-list page anchor

Our ChannelManager class takes care of everything related to channels. In the previous step, we waited for the client to synchronize channel information, and assigned an instance of TCHChannels(link takes you to an external page) to our ChannelManager.

Now we will get a list of light-weight channel descriptors to use for the list of channels in our application. We combine the channels the user has subscribed to (both public and private) with the list of publicly available channels. We do need to merge this list, and avoid adding duplicates. We also sort the channel list here alphabetically by the friendly name.

twiliochat/ChannelManager.swift

1
import UIKit
2
3
protocol ChannelManagerDelegate {
4
func reloadChannelDescriptorList()
5
}
6
7
class ChannelManager: NSObject {
8
static let sharedManager = ChannelManager()
9
10
static let defaultChannelUniqueName = "general"
11
static let defaultChannelName = "General Channel"
12
13
var delegate:ChannelManagerDelegate?
14
15
var channelsList:TCHChannels?
16
var channelDescriptors:NSOrderedSet?
17
var generalChannel:TCHChannel!
18
19
override init() {
20
super.init()
21
channelDescriptors = NSMutableOrderedSet()
22
}
23
24
// MARK: - General channel
25
26
func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
27
28
let uniqueName = ChannelManager.defaultChannelUniqueName
29
if let channelsList = self.channelsList {
30
channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in
31
self.generalChannel = channel
32
33
if self.generalChannel != nil {
34
self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)
35
} else {
36
self.createGeneralChatRoomWithCompletion { succeeded in
37
if (succeeded) {
38
self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)
39
return
40
}
41
42
completion(false)
43
}
44
}
45
}
46
}
47
}
48
49
func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {
50
generalChannel.join { result in
51
if ((result.isSuccessful()) && name != nil) {
52
self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)
53
return
54
}
55
completion((result.isSuccessful()))
56
}
57
}
58
59
func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
60
let channelName = ChannelManager.defaultChannelName
61
let options = [
62
TCHChannelOptionFriendlyName: channelName,
63
TCHChannelOptionType: TCHChannelType.public.rawValue
64
] as [String : Any]
65
channelsList!.createChannel(options: options) { result, channel in
66
if (result.isSuccessful()) {
67
self.generalChannel = channel
68
}
69
completion((result.isSuccessful()))
70
}
71
}
72
73
func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {
74
generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in
75
completion((result.isSuccessful()))
76
}
77
}
78
79
// MARK: - Populate channel Descriptors
80
81
func populateChannelDescriptors() {
82
83
channelsList?.userChannelDescriptors { result, paginator in
84
guard let paginator = paginator else {
85
return
86
}
87
88
let newChannelDescriptors = NSMutableOrderedSet()
89
newChannelDescriptors.addObjects(from: paginator.items())
90
self.channelsList?.publicChannelDescriptors { result, paginator in
91
guard let paginator = paginator else {
92
return
93
}
94
95
// de-dupe channel list
96
let channelIds = NSMutableSet()
97
for descriptor in newChannelDescriptors {
98
if let descriptor = descriptor as? TCHChannelDescriptor {
99
if let sid = descriptor.sid {
100
channelIds.add(sid)
101
}
102
}
103
}
104
for descriptor in paginator.items() {
105
if let sid = descriptor.sid {
106
if !channelIds.contains(sid) {
107
channelIds.add(sid)
108
newChannelDescriptors.add(descriptor)
109
}
110
}
111
}
112
113
114
// sort the descriptors
115
let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))
116
let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)
117
newChannelDescriptors.sort(using: [descriptor])
118
119
self.channelDescriptors = newChannelDescriptors
120
121
if let delegate = self.delegate {
122
delegate.reloadChannelDescriptorList()
123
}
124
}
125
}
126
}
127
128
129
// MARK: - Create channel
130
131
func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {
132
if (name == ChannelManager.defaultChannelName) {
133
completion(false, nil)
134
return
135
}
136
137
let channelOptions = [
138
TCHChannelOptionFriendlyName: name,
139
TCHChannelOptionType: TCHChannelType.public.rawValue
140
] as [String : Any]
141
UIApplication.shared.isNetworkActivityIndicatorVisible = true;
142
self.channelsList?.createChannel(options: channelOptions) { result, channel in
143
UIApplication.shared.isNetworkActivityIndicatorVisible = false
144
completion((result.isSuccessful()), channel)
145
}
146
}
147
}
148
149
// MARK: - TwilioChatClientDelegate
150
extension ChannelManager : TwilioChatClientDelegate {
151
func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {
152
DispatchQueue.main.async {
153
self.populateChannelDescriptors()
154
}
155
}
156
157
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {
158
DispatchQueue.main.async {
159
self.delegate?.reloadChannelDescriptorList()
160
}
161
}
162
163
func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
164
DispatchQueue.main.async {
165
self.populateChannelDescriptors()
166
}
167
168
}
169
170
func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {
171
}
172
}
173

Let's see how we can listen to events from the chat client so we can update our app's state.


The Programmable Chat Client will trigger events such as channelAdded(link takes you to an external page) or channelDeleted on our application. Given the creation or deletion of a channel, we'll reload the channel list in the reveal controller. If a channel is deleted and we were currently joined to that channel, the application will automatically join the general channel.

ChannelManager is a TwilioChatClientDelegate. In this class we implement the delegate methods, but we also allow MenuViewController class to be a delegate of ChannelManager, so it can listen to client events too.

Listen for Client Events

listen-for-client-events page anchor

twiliochat/ChannelManager.swift

1
import UIKit
2
3
protocol ChannelManagerDelegate {
4
func reloadChannelDescriptorList()
5
}
6
7
class ChannelManager: NSObject {
8
static let sharedManager = ChannelManager()
9
10
static let defaultChannelUniqueName = "general"
11
static let defaultChannelName = "General Channel"
12
13
var delegate:ChannelManagerDelegate?
14
15
var channelsList:TCHChannels?
16
var channelDescriptors:NSOrderedSet?
17
var generalChannel:TCHChannel!
18
19
override init() {
20
super.init()
21
channelDescriptors = NSMutableOrderedSet()
22
}
23
24
// MARK: - General channel
25
26
func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
27
28
let uniqueName = ChannelManager.defaultChannelUniqueName
29
if let channelsList = self.channelsList {
30
channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in
31
self.generalChannel = channel
32
33
if self.generalChannel != nil {
34
self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)
35
} else {
36
self.createGeneralChatRoomWithCompletion { succeeded in
37
if (succeeded) {
38
self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)
39
return
40
}
41
42
completion(false)
43
}
44
}
45
}
46
}
47
}
48
49
func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {
50
generalChannel.join { result in
51
if ((result.isSuccessful()) && name != nil) {
52
self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)
53
return
54
}
55
completion((result.isSuccessful()))
56
}
57
}
58
59
func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
60
let channelName = ChannelManager.defaultChannelName
61
let options = [
62
TCHChannelOptionFriendlyName: channelName,
63
TCHChannelOptionType: TCHChannelType.public.rawValue
64
] as [String : Any]
65
channelsList!.createChannel(options: options) { result, channel in
66
if (result.isSuccessful()) {
67
self.generalChannel = channel
68
}
69
completion((result.isSuccessful()))
70
}
71
}
72
73
func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {
74
generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in
75
completion((result.isSuccessful()))
76
}
77
}
78
79
// MARK: - Populate channel Descriptors
80
81
func populateChannelDescriptors() {
82
83
channelsList?.userChannelDescriptors { result, paginator in
84
guard let paginator = paginator else {
85
return
86
}
87
88
let newChannelDescriptors = NSMutableOrderedSet()
89
newChannelDescriptors.addObjects(from: paginator.items())
90
self.channelsList?.publicChannelDescriptors { result, paginator in
91
guard let paginator = paginator else {
92
return
93
}
94
95
// de-dupe channel list
96
let channelIds = NSMutableSet()
97
for descriptor in newChannelDescriptors {
98
if let descriptor = descriptor as? TCHChannelDescriptor {
99
if let sid = descriptor.sid {
100
channelIds.add(sid)
101
}
102
}
103
}
104
for descriptor in paginator.items() {
105
if let sid = descriptor.sid {
106
if !channelIds.contains(sid) {
107
channelIds.add(sid)
108
newChannelDescriptors.add(descriptor)
109
}
110
}
111
}
112
113
114
// sort the descriptors
115
let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))
116
let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)
117
newChannelDescriptors.sort(using: [descriptor])
118
119
self.channelDescriptors = newChannelDescriptors
120
121
if let delegate = self.delegate {
122
delegate.reloadChannelDescriptorList()
123
}
124
}
125
}
126
}
127
128
129
// MARK: - Create channel
130
131
func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {
132
if (name == ChannelManager.defaultChannelName) {
133
completion(false, nil)
134
return
135
}
136
137
let channelOptions = [
138
TCHChannelOptionFriendlyName: name,
139
TCHChannelOptionType: TCHChannelType.public.rawValue
140
] as [String : Any]
141
UIApplication.shared.isNetworkActivityIndicatorVisible = true;
142
self.channelsList?.createChannel(options: channelOptions) { result, channel in
143
UIApplication.shared.isNetworkActivityIndicatorVisible = false
144
completion((result.isSuccessful()), channel)
145
}
146
}
147
}
148
149
// MARK: - TwilioChatClientDelegate
150
extension ChannelManager : TwilioChatClientDelegate {
151
func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {
152
DispatchQueue.main.async {
153
self.populateChannelDescriptors()
154
}
155
}
156
157
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {
158
DispatchQueue.main.async {
159
self.delegate?.reloadChannelDescriptorList()
160
}
161
}
162
163
func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
164
DispatchQueue.main.async {
165
self.populateChannelDescriptors()
166
}
167
168
}
169
170
func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {
171
}
172
}
173

Next, we need a default channel.


Join the General Channel

join-the-general-channel page anchor

This application will try to join a channel called "General Channel" when it starts. If the channel doesn't exist, it'll create one with that name. The scope of this example application will show you how to work only with public channels, but the Programmable Chat client allows you to create private channels and handle invitations.

Once you have joined a channel, you can register a class as the TCHChannelDelegate so you can start listening to events such as messageAdded or memberJoined. We'll show you how to do this in the next step.

Join or Create a General Channel

join-or-create-a-general-channel page anchor

twiliochat/ChannelManager.swift

1
import UIKit
2
3
protocol ChannelManagerDelegate {
4
func reloadChannelDescriptorList()
5
}
6
7
class ChannelManager: NSObject {
8
static let sharedManager = ChannelManager()
9
10
static let defaultChannelUniqueName = "general"
11
static let defaultChannelName = "General Channel"
12
13
var delegate:ChannelManagerDelegate?
14
15
var channelsList:TCHChannels?
16
var channelDescriptors:NSOrderedSet?
17
var generalChannel:TCHChannel!
18
19
override init() {
20
super.init()
21
channelDescriptors = NSMutableOrderedSet()
22
}
23
24
// MARK: - General channel
25
26
func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
27
28
let uniqueName = ChannelManager.defaultChannelUniqueName
29
if let channelsList = self.channelsList {
30
channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in
31
self.generalChannel = channel
32
33
if self.generalChannel != nil {
34
self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)
35
} else {
36
self.createGeneralChatRoomWithCompletion { succeeded in
37
if (succeeded) {
38
self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)
39
return
40
}
41
42
completion(false)
43
}
44
}
45
}
46
}
47
}
48
49
func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {
50
generalChannel.join { result in
51
if ((result.isSuccessful()) && name != nil) {
52
self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)
53
return
54
}
55
completion((result.isSuccessful()))
56
}
57
}
58
59
func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
60
let channelName = ChannelManager.defaultChannelName
61
let options = [
62
TCHChannelOptionFriendlyName: channelName,
63
TCHChannelOptionType: TCHChannelType.public.rawValue
64
] as [String : Any]
65
channelsList!.createChannel(options: options) { result, channel in
66
if (result.isSuccessful()) {
67
self.generalChannel = channel
68
}
69
completion((result.isSuccessful()))
70
}
71
}
72
73
func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {
74
generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in
75
completion((result.isSuccessful()))
76
}
77
}
78
79
// MARK: - Populate channel Descriptors
80
81
func populateChannelDescriptors() {
82
83
channelsList?.userChannelDescriptors { result, paginator in
84
guard let paginator = paginator else {
85
return
86
}
87
88
let newChannelDescriptors = NSMutableOrderedSet()
89
newChannelDescriptors.addObjects(from: paginator.items())
90
self.channelsList?.publicChannelDescriptors { result, paginator in
91
guard let paginator = paginator else {
92
return
93
}
94
95
// de-dupe channel list
96
let channelIds = NSMutableSet()
97
for descriptor in newChannelDescriptors {
98
if let descriptor = descriptor as? TCHChannelDescriptor {
99
if let sid = descriptor.sid {
100
channelIds.add(sid)
101
}
102
}
103
}
104
for descriptor in paginator.items() {
105
if let sid = descriptor.sid {
106
if !channelIds.contains(sid) {
107
channelIds.add(sid)
108
newChannelDescriptors.add(descriptor)
109
}
110
}
111
}
112
113
114
// sort the descriptors
115
let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))
116
let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)
117
newChannelDescriptors.sort(using: [descriptor])
118
119
self.channelDescriptors = newChannelDescriptors
120
121
if let delegate = self.delegate {
122
delegate.reloadChannelDescriptorList()
123
}
124
}
125
}
126
}
127
128
129
// MARK: - Create channel
130
131
func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {
132
if (name == ChannelManager.defaultChannelName) {
133
completion(false, nil)
134
return
135
}
136
137
let channelOptions = [
138
TCHChannelOptionFriendlyName: name,
139
TCHChannelOptionType: TCHChannelType.public.rawValue
140
] as [String : Any]
141
UIApplication.shared.isNetworkActivityIndicatorVisible = true;
142
self.channelsList?.createChannel(options: channelOptions) { result, channel in
143
UIApplication.shared.isNetworkActivityIndicatorVisible = false
144
completion((result.isSuccessful()), channel)
145
}
146
}
147
}
148
149
// MARK: - TwilioChatClientDelegate
150
extension ChannelManager : TwilioChatClientDelegate {
151
func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {
152
DispatchQueue.main.async {
153
self.populateChannelDescriptors()
154
}
155
}
156
157
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {
158
DispatchQueue.main.async {
159
self.delegate?.reloadChannelDescriptorList()
160
}
161
}
162
163
func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
164
DispatchQueue.main.async {
165
self.populateChannelDescriptors()
166
}
167
168
}
169
170
func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {
171
}
172
}
173

Now let's listen for some channel events.


Listen to Channel Events

listen-to-channel-events page anchor

We registered MainChatViewController as the TCHChannelDelegate, and here we implemented the following methods that listen to channel events:

  • messageAdded: When someone sends a message to the channel you are connected to.
  • channelDeleted: When someone deletes a channel.
  • memberJoined: When someone joins the channel.
  • memberLeft: When someone leaves the channel.
  • synchronizationStatusChanged: When channel synchronization status changes.

As you may have noticed, each one of these methods includes useful objects as parameters. One example is the actual message that was added to the channel.

twiliochat/MainChatViewController.swift

1
import UIKit
2
import SlackTextViewController
3
4
class MainChatViewController: SLKTextViewController {
5
static let TWCChatCellIdentifier = "ChatTableCell"
6
static let TWCChatStatusCellIdentifier = "ChatStatusTableCell"
7
8
static let TWCOpenGeneralChannelSegue = "OpenGeneralChat"
9
static let TWCLabelTag = 200
10
11
var _channel:TCHChannel!
12
var channel:TCHChannel! {
13
get {
14
return _channel
15
}
16
set(channel) {
17
_channel = channel
18
title = _channel.friendlyName
19
_channel.delegate = self
20
21
if _channel == ChannelManager.sharedManager.generalChannel {
22
navigationItem.rightBarButtonItem = nil
23
}
24
25
joinChannel()
26
}
27
}
28
29
var messages:Set<TCHMessage> = Set<TCHMessage>()
30
var sortedMessages:[TCHMessage]!
31
32
@IBOutlet weak var revealButtonItem: UIBarButtonItem!
33
@IBOutlet weak var actionButtonItem: UIBarButtonItem!
34
35
override func viewDidLoad() {
36
super.viewDidLoad()
37
38
if (revealViewController() != nil) {
39
revealButtonItem.target = revealViewController()
40
revealButtonItem.action = #selector(SWRevealViewController.revealToggle(_:))
41
navigationController?.navigationBar.addGestureRecognizer(revealViewController().panGestureRecognizer())
42
revealViewController().rearViewRevealOverdraw = 0
43
}
44
45
bounces = true
46
shakeToClearEnabled = true
47
isKeyboardPanningEnabled = true
48
shouldScrollToBottomAfterKeyboardShows = false
49
isInverted = true
50
51
let cellNib = UINib(nibName: MainChatViewController.TWCChatCellIdentifier, bundle: nil)
52
tableView!.register(cellNib, forCellReuseIdentifier:MainChatViewController.TWCChatCellIdentifier)
53
54
let cellStatusNib = UINib(nibName: MainChatViewController.TWCChatStatusCellIdentifier, bundle: nil)
55
tableView!.register(cellStatusNib, forCellReuseIdentifier:MainChatViewController.TWCChatStatusCellIdentifier)
56
57
textInputbar.autoHideRightButton = true
58
textInputbar.maxCharCount = 256
59
textInputbar.counterStyle = .split
60
textInputbar.counterPosition = .top
61
62
let font = UIFont(name:"Avenir-Light", size:14)
63
textView.font = font
64
65
rightButton.setTitleColor(UIColor(red:0.973, green:0.557, blue:0.502, alpha:1), for: .normal)
66
67
if let font = UIFont(name:"Avenir-Heavy", size:17) {
68
navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: font]
69
}
70
71
tableView!.allowsSelection = false
72
tableView!.estimatedRowHeight = 70
73
tableView!.rowHeight = UITableView.automaticDimension
74
tableView!.separatorStyle = .none
75
76
if channel == nil {
77
channel = ChannelManager.sharedManager.generalChannel
78
}
79
}
80
81
override func viewDidLayoutSubviews() {
82
super.viewDidLayoutSubviews()
83
84
// required for iOS 11
85
textInputbar.bringSubviewToFront(textInputbar.textView)
86
textInputbar.bringSubviewToFront(textInputbar.leftButton)
87
textInputbar.bringSubviewToFront(textInputbar.rightButton)
88
89
}
90
91
override func viewDidAppear(_ animated: Bool) {
92
super.viewDidAppear(animated)
93
scrollToBottom()
94
}
95
96
override func numberOfSections(in tableView: UITableView) -> Int {
97
return 1
98
}
99
100
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: NSInteger) -> Int {
101
return messages.count
102
}
103
104
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
105
var cell:UITableViewCell
106
107
let message = sortedMessages[indexPath.row]
108
109
if let statusMessage = message as? StatusMessage {
110
cell = getStatusCellForTableView(tableView: tableView, forIndexPath:indexPath, message:statusMessage)
111
}
112
else {
113
cell = getChatCellForTableView(tableView: tableView, forIndexPath:indexPath, message:message)
114
}
115
116
cell.transform = tableView.transform
117
return cell
118
}
119
120
func getChatCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: TCHMessage) -> UITableViewCell {
121
let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatCellIdentifier, for:indexPath as IndexPath)
122
123
let chatCell: ChatTableCell = cell as! ChatTableCell
124
let date = NSDate.dateWithISO8601String(dateString: message.dateCreated ?? "")
125
let timestamp = DateTodayFormatter().stringFromDate(date: date)
126
127
chatCell.setUser(user: message.author ?? "[Unknown author]", message: message.body, date: timestamp ?? "[Unknown date]")
128
129
return chatCell
130
}
131
132
func getStatusCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: StatusMessage) -> UITableViewCell {
133
let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatStatusCellIdentifier, for:indexPath as IndexPath)
134
135
let label = cell.viewWithTag(MainChatViewController.TWCLabelTag) as! UILabel
136
let memberStatus = (message.status! == .Joined) ? "joined" : "left"
137
label.text = "User \(message.statusMember.identity ?? "[Unknown user]") has \(memberStatus)"
138
return cell
139
}
140
141
func joinChannel() {
142
setViewOnHold(onHold: true)
143
144
if channel.status != .joined {
145
channel.join { result in
146
print("Channel Joined")
147
}
148
return
149
}
150
151
loadMessages()
152
setViewOnHold(onHold: false)
153
}
154
155
// Disable user input and show activity indicator
156
func setViewOnHold(onHold: Bool) {
157
self.isTextInputbarHidden = onHold;
158
UIApplication.shared.isNetworkActivityIndicatorVisible = onHold;
159
}
160
161
override func didPressRightButton(_ sender: Any!) {
162
textView.refreshFirstResponder()
163
sendMessage(inputMessage: textView.text)
164
super.didPressRightButton(sender)
165
}
166
167
// MARK: - Chat Service
168
169
func sendMessage(inputMessage: String) {
170
let messageOptions = TCHMessageOptions().withBody(inputMessage)
171
channel.messages?.sendMessage(with: messageOptions, completion: nil)
172
}
173
174
func addMessages(newMessages:Set<TCHMessage>) {
175
messages = messages.union(newMessages)
176
sortMessages()
177
DispatchQueue.main.async {
178
self.tableView!.reloadData()
179
if self.messages.count > 0 {
180
self.scrollToBottom()
181
}
182
}
183
}
184
185
func sortMessages() {
186
sortedMessages = messages.sorted { (a, b) -> Bool in
187
(a.dateCreated ?? "") > (b.dateCreated ?? "")
188
}
189
}
190
191
func loadMessages() {
192
messages.removeAll()
193
if channel.synchronizationStatus == .all {
194
channel.messages?.getLastWithCount(100) { (result, items) in
195
self.addMessages(newMessages: Set(items!))
196
}
197
}
198
}
199
200
func scrollToBottom() {
201
if messages.count > 0 {
202
let indexPath = IndexPath(row: 0, section: 0)
203
tableView!.scrollToRow(at: indexPath, at: .bottom, animated: true)
204
}
205
}
206
207
func leaveChannel() {
208
channel.leave { result in
209
if (result.isSuccessful()) {
210
let menuViewController = self.revealViewController().rearViewController as! MenuViewController
211
menuViewController.deselectSelectedChannel()
212
self.revealViewController().rearViewController.performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)
213
}
214
}
215
}
216
217
// MARK: - Actions
218
219
@IBAction func actionButtonTouched(_ sender: UIBarButtonItem) {
220
leaveChannel()
221
}
222
223
@IBAction func revealButtonTouched(_ sender: AnyObject) {
224
revealViewController().revealToggle(animated: true)
225
}
226
}
227
228
extension MainChatViewController : TCHChannelDelegate {
229
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, messageAdded message: TCHMessage) {
230
if !messages.contains(message) {
231
addMessages(newMessages: [message])
232
}
233
}
234
235
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberJoined member: TCHMember) {
236
addMessages(newMessages: [StatusMessage(statusMember:member, status:.Joined)])
237
}
238
239
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberLeft member: TCHMember) {
240
addMessages(newMessages: [StatusMessage(statusMember:member, status:.Left)])
241
}
242
243
func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
244
DispatchQueue.main.async {
245
if channel == self.channel {
246
self.revealViewController().rearViewController
247
.performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)
248
}
249
}
250
}
251
252
func chatClient(_ client: TwilioChatClient,
253
channel: TCHChannel,
254
synchronizationStatusUpdated status: TCHChannelSynchronizationStatus) {
255
if status == .all {
256
loadMessages()
257
DispatchQueue.main.async {
258
self.tableView?.reloadData()
259
self.setViewOnHold(onHold: false)
260
}
261
}
262
}
263
}
264

We've actually got a real chat app going here, but let's make it more interesting with multiple channels.


The application uses SWRevealViewController(link takes you to an external page) to show a sidebar that contains a list of the channels created for that Twilio account.

When you tap on the name of a channel from the sidebar, that channel is set on the MainChatViewController. The joinChannel method takes care of joining to the selected channel and loading the messages.

twiliochat/MainChatViewController.swift

1
import UIKit
2
import SlackTextViewController
3
4
class MainChatViewController: SLKTextViewController {
5
static let TWCChatCellIdentifier = "ChatTableCell"
6
static let TWCChatStatusCellIdentifier = "ChatStatusTableCell"
7
8
static let TWCOpenGeneralChannelSegue = "OpenGeneralChat"
9
static let TWCLabelTag = 200
10
11
var _channel:TCHChannel!
12
var channel:TCHChannel! {
13
get {
14
return _channel
15
}
16
set(channel) {
17
_channel = channel
18
title = _channel.friendlyName
19
_channel.delegate = self
20
21
if _channel == ChannelManager.sharedManager.generalChannel {
22
navigationItem.rightBarButtonItem = nil
23
}
24
25
joinChannel()
26
}
27
}
28
29
var messages:Set<TCHMessage> = Set<TCHMessage>()
30
var sortedMessages:[TCHMessage]!
31
32
@IBOutlet weak var revealButtonItem: UIBarButtonItem!
33
@IBOutlet weak var actionButtonItem: UIBarButtonItem!
34
35
override func viewDidLoad() {
36
super.viewDidLoad()
37
38
if (revealViewController() != nil) {
39
revealButtonItem.target = revealViewController()
40
revealButtonItem.action = #selector(SWRevealViewController.revealToggle(_:))
41
navigationController?.navigationBar.addGestureRecognizer(revealViewController().panGestureRecognizer())
42
revealViewController().rearViewRevealOverdraw = 0
43
}
44
45
bounces = true
46
shakeToClearEnabled = true
47
isKeyboardPanningEnabled = true
48
shouldScrollToBottomAfterKeyboardShows = false
49
isInverted = true
50
51
let cellNib = UINib(nibName: MainChatViewController.TWCChatCellIdentifier, bundle: nil)
52
tableView!.register(cellNib, forCellReuseIdentifier:MainChatViewController.TWCChatCellIdentifier)
53
54
let cellStatusNib = UINib(nibName: MainChatViewController.TWCChatStatusCellIdentifier, bundle: nil)
55
tableView!.register(cellStatusNib, forCellReuseIdentifier:MainChatViewController.TWCChatStatusCellIdentifier)
56
57
textInputbar.autoHideRightButton = true
58
textInputbar.maxCharCount = 256
59
textInputbar.counterStyle = .split
60
textInputbar.counterPosition = .top
61
62
let font = UIFont(name:"Avenir-Light", size:14)
63
textView.font = font
64
65
rightButton.setTitleColor(UIColor(red:0.973, green:0.557, blue:0.502, alpha:1), for: .normal)
66
67
if let font = UIFont(name:"Avenir-Heavy", size:17) {
68
navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: font]
69
}
70
71
tableView!.allowsSelection = false
72
tableView!.estimatedRowHeight = 70
73
tableView!.rowHeight = UITableView.automaticDimension
74
tableView!.separatorStyle = .none
75
76
if channel == nil {
77
channel = ChannelManager.sharedManager.generalChannel
78
}
79
}
80
81
override func viewDidLayoutSubviews() {
82
super.viewDidLayoutSubviews()
83
84
// required for iOS 11
85
textInputbar.bringSubviewToFront(textInputbar.textView)
86
textInputbar.bringSubviewToFront(textInputbar.leftButton)
87
textInputbar.bringSubviewToFront(textInputbar.rightButton)
88
89
}
90
91
override func viewDidAppear(_ animated: Bool) {
92
super.viewDidAppear(animated)
93
scrollToBottom()
94
}
95
96
override func numberOfSections(in tableView: UITableView) -> Int {
97
return 1
98
}
99
100
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: NSInteger) -> Int {
101
return messages.count
102
}
103
104
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
105
var cell:UITableViewCell
106
107
let message = sortedMessages[indexPath.row]
108
109
if let statusMessage = message as? StatusMessage {
110
cell = getStatusCellForTableView(tableView: tableView, forIndexPath:indexPath, message:statusMessage)
111
}
112
else {
113
cell = getChatCellForTableView(tableView: tableView, forIndexPath:indexPath, message:message)
114
}
115
116
cell.transform = tableView.transform
117
return cell
118
}
119
120
func getChatCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: TCHMessage) -> UITableViewCell {
121
let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatCellIdentifier, for:indexPath as IndexPath)
122
123
let chatCell: ChatTableCell = cell as! ChatTableCell
124
let date = NSDate.dateWithISO8601String(dateString: message.dateCreated ?? "")
125
let timestamp = DateTodayFormatter().stringFromDate(date: date)
126
127
chatCell.setUser(user: message.author ?? "[Unknown author]", message: message.body, date: timestamp ?? "[Unknown date]")
128
129
return chatCell
130
}
131
132
func getStatusCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: StatusMessage) -> UITableViewCell {
133
let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatStatusCellIdentifier, for:indexPath as IndexPath)
134
135
let label = cell.viewWithTag(MainChatViewController.TWCLabelTag) as! UILabel
136
let memberStatus = (message.status! == .Joined) ? "joined" : "left"
137
label.text = "User \(message.statusMember.identity ?? "[Unknown user]") has \(memberStatus)"
138
return cell
139
}
140
141
func joinChannel() {
142
setViewOnHold(onHold: true)
143
144
if channel.status != .joined {
145
channel.join { result in
146
print("Channel Joined")
147
}
148
return
149
}
150
151
loadMessages()
152
setViewOnHold(onHold: false)
153
}
154
155
// Disable user input and show activity indicator
156
func setViewOnHold(onHold: Bool) {
157
self.isTextInputbarHidden = onHold;
158
UIApplication.shared.isNetworkActivityIndicatorVisible = onHold;
159
}
160
161
override func didPressRightButton(_ sender: Any!) {
162
textView.refreshFirstResponder()
163
sendMessage(inputMessage: textView.text)
164
super.didPressRightButton(sender)
165
}
166
167
// MARK: - Chat Service
168
169
func sendMessage(inputMessage: String) {
170
let messageOptions = TCHMessageOptions().withBody(inputMessage)
171
channel.messages?.sendMessage(with: messageOptions, completion: nil)
172
}
173
174
func addMessages(newMessages:Set<TCHMessage>) {
175
messages = messages.union(newMessages)
176
sortMessages()
177
DispatchQueue.main.async {
178
self.tableView!.reloadData()
179
if self.messages.count > 0 {
180
self.scrollToBottom()
181
}
182
}
183
}
184
185
func sortMessages() {
186
sortedMessages = messages.sorted { (a, b) -> Bool in
187
(a.dateCreated ?? "") > (b.dateCreated ?? "")
188
}
189
}
190
191
func loadMessages() {
192
messages.removeAll()
193
if channel.synchronizationStatus == .all {
194
channel.messages?.getLastWithCount(100) { (result, items) in
195
self.addMessages(newMessages: Set(items!))
196
}
197
}
198
}
199
200
func scrollToBottom() {
201
if messages.count > 0 {
202
let indexPath = IndexPath(row: 0, section: 0)
203
tableView!.scrollToRow(at: indexPath, at: .bottom, animated: true)
204
}
205
}
206
207
func leaveChannel() {
208
channel.leave { result in
209
if (result.isSuccessful()) {
210
let menuViewController = self.revealViewController().rearViewController as! MenuViewController
211
menuViewController.deselectSelectedChannel()
212
self.revealViewController().rearViewController.performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)
213
}
214
}
215
}
216
217
// MARK: - Actions
218
219
@IBAction func actionButtonTouched(_ sender: UIBarButtonItem) {
220
leaveChannel()
221
}
222
223
@IBAction func revealButtonTouched(_ sender: AnyObject) {
224
revealViewController().revealToggle(animated: true)
225
}
226
}
227
228
extension MainChatViewController : TCHChannelDelegate {
229
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, messageAdded message: TCHMessage) {
230
if !messages.contains(message) {
231
addMessages(newMessages: [message])
232
}
233
}
234
235
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberJoined member: TCHMember) {
236
addMessages(newMessages: [StatusMessage(statusMember:member, status:.Joined)])
237
}
238
239
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberLeft member: TCHMember) {
240
addMessages(newMessages: [StatusMessage(statusMember:member, status:.Left)])
241
}
242
243
func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
244
DispatchQueue.main.async {
245
if channel == self.channel {
246
self.revealViewController().rearViewController
247
.performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)
248
}
249
}
250
}
251
252
func chatClient(_ client: TwilioChatClient,
253
channel: TCHChannel,
254
synchronizationStatusUpdated status: TCHChannelSynchronizationStatus) {
255
if status == .all {
256
loadMessages()
257
DispatchQueue.main.async {
258
self.tableView?.reloadData()
259
self.setViewOnHold(onHold: false)
260
}
261
}
262
}
263
}
264

If we can join other channels, we'll need some way for a super user to create new channels (and delete old ones).


We use an input dialog so the user can type the name of the new channel. The only restriction here is that the user can't create a channel called "General Channel". Other than that, creating a channel involves calling createChannel and passing a dictionary with the new channel information.

twiliochat/ChannelManager.swift

1
import UIKit
2
3
protocol ChannelManagerDelegate {
4
func reloadChannelDescriptorList()
5
}
6
7
class ChannelManager: NSObject {
8
static let sharedManager = ChannelManager()
9
10
static let defaultChannelUniqueName = "general"
11
static let defaultChannelName = "General Channel"
12
13
var delegate:ChannelManagerDelegate?
14
15
var channelsList:TCHChannels?
16
var channelDescriptors:NSOrderedSet?
17
var generalChannel:TCHChannel!
18
19
override init() {
20
super.init()
21
channelDescriptors = NSMutableOrderedSet()
22
}
23
24
// MARK: - General channel
25
26
func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
27
28
let uniqueName = ChannelManager.defaultChannelUniqueName
29
if let channelsList = self.channelsList {
30
channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in
31
self.generalChannel = channel
32
33
if self.generalChannel != nil {
34
self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)
35
} else {
36
self.createGeneralChatRoomWithCompletion { succeeded in
37
if (succeeded) {
38
self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)
39
return
40
}
41
42
completion(false)
43
}
44
}
45
}
46
}
47
}
48
49
func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {
50
generalChannel.join { result in
51
if ((result.isSuccessful()) && name != nil) {
52
self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)
53
return
54
}
55
completion((result.isSuccessful()))
56
}
57
}
58
59
func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
60
let channelName = ChannelManager.defaultChannelName
61
let options = [
62
TCHChannelOptionFriendlyName: channelName,
63
TCHChannelOptionType: TCHChannelType.public.rawValue
64
] as [String : Any]
65
channelsList!.createChannel(options: options) { result, channel in
66
if (result.isSuccessful()) {
67
self.generalChannel = channel
68
}
69
completion((result.isSuccessful()))
70
}
71
}
72
73
func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {
74
generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in
75
completion((result.isSuccessful()))
76
}
77
}
78
79
// MARK: - Populate channel Descriptors
80
81
func populateChannelDescriptors() {
82
83
channelsList?.userChannelDescriptors { result, paginator in
84
guard let paginator = paginator else {
85
return
86
}
87
88
let newChannelDescriptors = NSMutableOrderedSet()
89
newChannelDescriptors.addObjects(from: paginator.items())
90
self.channelsList?.publicChannelDescriptors { result, paginator in
91
guard let paginator = paginator else {
92
return
93
}
94
95
// de-dupe channel list
96
let channelIds = NSMutableSet()
97
for descriptor in newChannelDescriptors {
98
if let descriptor = descriptor as? TCHChannelDescriptor {
99
if let sid = descriptor.sid {
100
channelIds.add(sid)
101
}
102
}
103
}
104
for descriptor in paginator.items() {
105
if let sid = descriptor.sid {
106
if !channelIds.contains(sid) {
107
channelIds.add(sid)
108
newChannelDescriptors.add(descriptor)
109
}
110
}
111
}
112
113
114
// sort the descriptors
115
let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))
116
let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)
117
newChannelDescriptors.sort(using: [descriptor])
118
119
self.channelDescriptors = newChannelDescriptors
120
121
if let delegate = self.delegate {
122
delegate.reloadChannelDescriptorList()
123
}
124
}
125
}
126
}
127
128
129
// MARK: - Create channel
130
131
func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {
132
if (name == ChannelManager.defaultChannelName) {
133
completion(false, nil)
134
return
135
}
136
137
let channelOptions = [
138
TCHChannelOptionFriendlyName: name,
139
TCHChannelOptionType: TCHChannelType.public.rawValue
140
] as [String : Any]
141
UIApplication.shared.isNetworkActivityIndicatorVisible = true;
142
self.channelsList?.createChannel(options: channelOptions) { result, channel in
143
UIApplication.shared.isNetworkActivityIndicatorVisible = false
144
completion((result.isSuccessful()), channel)
145
}
146
}
147
}
148
149
// MARK: - TwilioChatClientDelegate
150
extension ChannelManager : TwilioChatClientDelegate {
151
func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {
152
DispatchQueue.main.async {
153
self.populateChannelDescriptors()
154
}
155
}
156
157
func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {
158
DispatchQueue.main.async {
159
self.delegate?.reloadChannelDescriptorList()
160
}
161
}
162
163
func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
164
DispatchQueue.main.async {
165
self.populateChannelDescriptors()
166
}
167
168
}
169
170
func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {
171
}
172
}
173

Cool, we now know how to create a channel, let's say that we created a lot of channels by mistake. In that case, it would be useful to be able to delete those unnecessary channels. That's our next step!


Deleting a channel is easier than creating one. We'll use the UITableView ability to delete a cell. Once you have figured out what channel is meant to be deleted (from the selected cell index path), call the channel's method destroy.

twiliochat/MenuViewController.swift

1
import UIKit
2
3
class MenuViewController: UIViewController {
4
static let TWCOpenChannelSegue = "OpenChat"
5
static let TWCRefreshControlXOffset: CGFloat = 120
6
7
@IBOutlet weak var tableView: UITableView!
8
@IBOutlet weak var usernameLabel: UILabel!
9
10
var refreshControl: UIRefreshControl!
11
12
override func viewDidLoad() {
13
super.viewDidLoad()
14
15
let bgImage = UIImageView(image: UIImage(named:"home-bg"))
16
bgImage.frame = self.tableView.frame
17
tableView.backgroundView = bgImage
18
19
usernameLabel.text = MessagingManager.sharedManager().userIdentity
20
21
refreshControl = UIRefreshControl()
22
tableView.addSubview(refreshControl)
23
refreshControl.addTarget(self, action: #selector(MenuViewController.refreshChannels), for: .valueChanged)
24
refreshControl.tintColor = UIColor.white
25
26
self.refreshControl.frame.origin.x -= MenuViewController.TWCRefreshControlXOffset
27
ChannelManager.sharedManager.delegate = self
28
tableView.reloadData()
29
}
30
31
// MARK: - Internal methods
32
33
func loadingCellForTableView(tableView: UITableView) -> UITableViewCell {
34
return tableView.dequeueReusableCell(withIdentifier: "loadingCell")!
35
}
36
37
func channelCellForTableView(tableView: UITableView, atIndexPath indexPath: NSIndexPath) -> UITableViewCell {
38
let menuCell = tableView.dequeueReusableCell(withIdentifier: "channelCell", for: indexPath as IndexPath) as! MenuTableCell
39
40
if let channelDescriptor = ChannelManager.sharedManager.channelDescriptors![indexPath.row] as? TCHChannelDescriptor {
41
menuCell.channelName = channelDescriptor.friendlyName ?? "[Unknown channel name]"
42
} else {
43
menuCell.channelName = "[Unknown channel name]"
44
}
45
46
return menuCell
47
}
48
49
@objc func refreshChannels() {
50
refreshControl.beginRefreshing()
51
tableView.reloadData()
52
refreshControl.endRefreshing()
53
}
54
55
func deselectSelectedChannel() {
56
let selectedRow = tableView.indexPathForSelectedRow
57
if let row = selectedRow {
58
tableView.deselectRow(at: row, animated: true)
59
}
60
}
61
62
// MARK: - Channel
63
64
func createNewChannelDialog() {
65
InputDialogController.showWithTitle(title: "New Channel",
66
message: "Enter a name for this channel",
67
placeholder: "Name",
68
presenter: self) { text in
69
ChannelManager.sharedManager.createChannelWithName(name: text, completion: { _,_ in
70
ChannelManager.sharedManager.populateChannelDescriptors()
71
})
72
}
73
}
74
75
// MARK: Logout
76
77
func promptLogout() {
78
let alert = UIAlertController(title: nil, message: "You are about to Logout", preferredStyle: .alert)
79
80
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
81
let confirmAction = UIAlertAction(title: "Confirm", style: .default) { action in
82
self.logOut()
83
}
84
85
alert.addAction(cancelAction)
86
alert.addAction(confirmAction)
87
present(alert, animated: true, completion: nil)
88
}
89
90
func logOut() {
91
MessagingManager.sharedManager().logout()
92
MessagingManager.sharedManager().presentRootViewController()
93
}
94
95
// MARK: - Actions
96
97
@IBAction func logoutButtonTouched(_ sender: UIButton) {
98
promptLogout()
99
}
100
101
@IBAction func newChannelButtonTouched(_ sender: UIButton) {
102
createNewChannelDialog()
103
}
104
105
// MARK: - Navigation
106
107
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
108
if segue.identifier == MenuViewController.TWCOpenChannelSegue {
109
let indexPath = sender as! NSIndexPath
110
111
let channelDescriptor = ChannelManager.sharedManager.channelDescriptors![indexPath.row] as! TCHChannelDescriptor
112
let navigationController = segue.destination as! UINavigationController
113
114
channelDescriptor.channel { (result, channel) in
115
if let channel = channel {
116
(navigationController.visibleViewController as! MainChatViewController).channel = channel
117
}
118
}
119
120
}
121
}
122
123
// MARK: - Style
124
125
override var preferredStatusBarStyle: UIStatusBarStyle {
126
return .lightContent
127
}
128
}
129
130
// MARK: - UITableViewDataSource
131
extension MenuViewController : UITableViewDataSource {
132
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
133
if let channelDescriptors = ChannelManager.sharedManager.channelDescriptors {
134
print (channelDescriptors.count)
135
return channelDescriptors.count
136
}
137
return 1
138
}
139
140
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
141
let cell: UITableViewCell
142
143
if ChannelManager.sharedManager.channelDescriptors == nil {
144
cell = loadingCellForTableView(tableView: tableView)
145
}
146
else {
147
cell = channelCellForTableView(tableView: tableView, atIndexPath: indexPath as NSIndexPath)
148
}
149
150
cell.layoutIfNeeded()
151
return cell
152
}
153
154
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
155
if let channel = ChannelManager.sharedManager.channelDescriptors?.object(at: indexPath.row) as? TCHChannel {
156
return channel != ChannelManager.sharedManager.generalChannel
157
}
158
return false
159
}
160
161
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle,
162
forRowAt indexPath: IndexPath) {
163
if editingStyle != .delete {
164
return
165
}
166
if let channel = ChannelManager.sharedManager.channelDescriptors?.object(at: indexPath.row) as? TCHChannel {
167
channel.destroy { result in
168
if (result.isSuccessful()) {
169
tableView.reloadData()
170
}
171
else {
172
AlertDialogController.showAlertWithMessage(message: "You can not delete this channel", title: nil, presenter: self)
173
}
174
}
175
}
176
}
177
}
178
179
// MARK: - UITableViewDelegate
180
extension MenuViewController : UITableViewDelegate {
181
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
182
tableView.deselectRow(at: indexPath, animated: true)
183
performSegue(withIdentifier: MenuViewController.TWCOpenChannelSegue, sender: indexPath)
184
}
185
}
186
187
188
// MARK: - ChannelManagerDelegate
189
extension MenuViewController : ChannelManagerDelegate {
190
func reloadChannelDescriptorList() {
191
tableView.reloadData()
192
}
193
}
194

That's it! We've built an iOS application with Swift. Now you are more than prepared to set up your own chat application.


If you are an iOS developer working with Twilio, you might want to check out this other project:

Notifications Quickstart(link takes you to an external page)

Twilio Notifications for iOS Quickstart using Swift

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio(link takes you to an external page) to let us know what you think.

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.