Skip to contentSkip to navigationSkip to topbar
On this page

Chat with iOS and Objective-C


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

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

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

  1. Twilio Programmable Chat(link takes you to an external page) 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 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 it as required.


Initialize the Chat Client

initialize-the-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 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:

We use AFNetworking(link takes you to an external page) to make a request to our server and get the access token.

Fetch Access Token

fetch-access-token page anchor

twiliochat/MessagingManager.m

1
#import "MessagingManager.h"
2
#import "ChannelManager.h"
3
#import "SessionManager.h"
4
#import "TokenRequestHandler.h"
5
6
@interface MessagingManager ()
7
@property (strong, nonatomic) TwilioChatClient *client;
8
@property (nonatomic, getter=isConnected) BOOL connected;
9
@end
10
11
static NSString * const TWCLoginViewControllerName = @"LoginViewController";
12
static NSString * const TWCMainViewControllerName = @"RevealViewController";
13
14
static NSString * const TWCTokenKey = @"token";
15
16
@implementation MessagingManager
17
+ (instancetype)sharedManager {
18
static MessagingManager *sharedMyManager = nil;
19
static dispatch_once_t onceToken;
20
dispatch_once(&onceToken, ^{
21
sharedMyManager = [[self alloc] init];
22
});
23
return sharedMyManager;
24
}
25
26
- (instancetype)init {
27
self.delegate = [ChannelManager sharedManager];
28
return self;
29
}
30
31
# pragma mark Present view controllers
32
33
- (void)presentRootViewController {
34
if (!self.isLoggedIn) {
35
[self presentViewControllerByName:TWCLoginViewControllerName];
36
return;
37
}
38
if (!self.isConnected) {
39
[self connectClientWithCompletion:^(BOOL success, NSError *error) {
40
if (success) {
41
NSLog(@"Successfully connected chat client");
42
}
43
}];
44
}
45
46
}
47
48
- (void)presentViewControllerByName:(NSString *)viewController {
49
[self presentViewController:[[self storyboardWithName:@"Main"] instantiateViewControllerWithIdentifier:viewController]];
50
}
51
52
- (void)presentLaunchScreen {
53
[self presentViewController:[[self storyboardWithName:@"LaunchScreen"] instantiateInitialViewController]];
54
}
55
56
- (void)presentViewController:(UIViewController *)viewController {
57
UIWindow *window = [[UIApplication sharedApplication].delegate window];
58
window.rootViewController = viewController;
59
}
60
61
- (UIStoryboard *)storyboardWithName:(NSString *)name {
62
return [UIStoryboard storyboardWithName: name bundle: [NSBundle mainBundle]];
63
}
64
65
# pragma mark User and session management
66
67
- (BOOL)isLoggedIn {
68
return [SessionManager isLoggedIn];
69
}
70
71
- (void)loginWithUsername:(NSString *)username
72
completion:(StatusWithErrorHandler)completion {
73
[SessionManager loginWithUsername:username];
74
[self connectClientWithCompletion:^(BOOL success, NSError *error) {
75
if (success) {
76
[self presentViewControllerByName:TWCMainViewControllerName];
77
}
78
completion(success, error);
79
}];
80
}
81
82
- (void)logout {
83
[SessionManager logout];
84
self.connected = NO;
85
86
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
87
[self.client shutdown];
88
self.client = nil;
89
});
90
}
91
92
# pragma mark Twilio client
93
94
- (void)connectClientWithCompletion:(StatusWithErrorHandler)completion {
95
if (self.client) {
96
[self logout];
97
}
98
99
[self requestTokenWithCompletion:^(BOOL succeeded, NSString *token) {
100
if (succeeded) {
101
[self initializeClientWithToken:token];
102
if (completion) completion(succeeded, nil);
103
}
104
else {
105
NSError *error = [self errorWithDescription:@"Could not get access token" code:301];
106
if (completion) completion(succeeded, error);
107
}
108
}];
109
}
110
111
- (void)initializeClientWithToken:(NSString *)token {
112
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
113
114
[TwilioChatClient chatClientWithToken:token
115
properties:nil
116
delegate:self
117
completion:^(TCHResult * _Nonnull result, TwilioChatClient * _Nullable chatClient) {
118
if (result.isSuccessful) {
119
self.client = chatClient;
120
self.connected = YES;
121
122
}
123
}];
124
}
125
126
- (void)requestTokenWithCompletion:(StatusWithTokenHandler)completion {
127
NSString *uuid = [[UIDevice currentDevice] identifierForVendor].UUIDString;
128
NSDictionary *parameters = @{@"device": uuid, @"identity": [SessionManager getUsername]};
129
130
[TokenRequestHandler fetchTokenWithParams:parameters completion:^(NSDictionary *results, NSError *error) {
131
NSString *token = [results objectForKey:TWCTokenKey];
132
BOOL errorCondition = error || !token;
133
134
if (completion) completion(!errorCondition, token);
135
}];
136
}
137
138
- (void)loadGeneralChatRoomWithCompletion:(StatusWithErrorHandler)completion {
139
[[ChannelManager sharedManager] joinGeneralChatRoomWithCompletion:^(BOOL succeeded) {
140
if (succeeded)
141
{
142
if (completion) completion(succeeded, nil);
143
}
144
else {
145
NSError *error = [self errorWithDescription:@"Could not join General channel" code:300];
146
if (completion) completion(succeeded, error);
147
}
148
}];
149
}
150
151
- (NSError *)errorWithDescription:(NSString *)description code:(NSInteger)code {
152
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};
153
NSError *error = [NSError errorWithDomain:@"app" code:code userInfo:userInfo];
154
return error;
155
}
156
157
#pragma mark Internal helpers
158
159
- (NSString *)userIdentity {
160
return [SessionManager getUsername];
161
}
162
163
- (void)refreshChatToken:(TwilioChatClient*)client {
164
[self requestTokenWithCompletion:^(BOOL succeeded, NSString *token) {
165
if (succeeded) {
166
[client updateToken:token completion:^(TCHResult * _Nonnull result) {
167
if (result.isSuccessful) {
168
169
}
170
}];
171
}
172
else {
173
NSLog(@"Error while trying to get new access token");
174
}
175
}];
176
}
177
178
#pragma mark TwilioChatClientDelegate
179
180
- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel {
181
[self.delegate chatClient:client channelAdded:channel];
182
}
183
184
- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {
185
[self.delegate chatClient:client channelDeleted:channel];
186
}
187
188
- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {
189
if (status == TCHClientSynchronizationStatusCompleted) {
190
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
191
[ChannelManager sharedManager].channelsList = client.channelsList;
192
[[ChannelManager sharedManager] populateChannels];
193
[self loadGeneralChatRoomWithCompletion:^(BOOL success, NSError *error) {
194
if (success) {
195
[self presentViewControllerByName:TWCMainViewControllerName];
196
}
197
}];
198
}
199
[self.delegate chatClient:client synchronizationStatusUpdated:status];
200
}
201
202
- (void)chatClientTokenWillExpire:(TwilioChatClient *)client {
203
[self refreshChatToken:client];
204
}
205
206
- (void)chatClientTokenExpired:(TwilioChatClient *)client {
207
[self refreshChatToken:client];
208
}
209
210
@end
211

Now it's time to synchronize your Twilio client.


Synchronize the Chat Client

synchronize-the-chat-client page anchor

The synchronizationStatusUpdated 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. Otherwise, calling client.channelsList()(link takes you to an external page) will return nil.

twiliochat/MessagingManager.m

1
#import "MessagingManager.h"
2
#import "ChannelManager.h"
3
#import "SessionManager.h"
4
#import "TokenRequestHandler.h"
5
6
@interface MessagingManager ()
7
@property (strong, nonatomic) TwilioChatClient *client;
8
@property (nonatomic, getter=isConnected) BOOL connected;
9
@end
10
11
static NSString * const TWCLoginViewControllerName = @"LoginViewController";
12
static NSString * const TWCMainViewControllerName = @"RevealViewController";
13
14
static NSString * const TWCTokenKey = @"token";
15
16
@implementation MessagingManager
17
+ (instancetype)sharedManager {
18
static MessagingManager *sharedMyManager = nil;
19
static dispatch_once_t onceToken;
20
dispatch_once(&onceToken, ^{
21
sharedMyManager = [[self alloc] init];
22
});
23
return sharedMyManager;
24
}
25
26
- (instancetype)init {
27
self.delegate = [ChannelManager sharedManager];
28
return self;
29
}
30
31
# pragma mark Present view controllers
32
33
- (void)presentRootViewController {
34
if (!self.isLoggedIn) {
35
[self presentViewControllerByName:TWCLoginViewControllerName];
36
return;
37
}
38
if (!self.isConnected) {
39
[self connectClientWithCompletion:^(BOOL success, NSError *error) {
40
if (success) {
41
NSLog(@"Successfully connected chat client");
42
}
43
}];
44
}
45
46
}
47
48
- (void)presentViewControllerByName:(NSString *)viewController {
49
[self presentViewController:[[self storyboardWithName:@"Main"] instantiateViewControllerWithIdentifier:viewController]];
50
}
51
52
- (void)presentLaunchScreen {
53
[self presentViewController:[[self storyboardWithName:@"LaunchScreen"] instantiateInitialViewController]];
54
}
55
56
- (void)presentViewController:(UIViewController *)viewController {
57
UIWindow *window = [[UIApplication sharedApplication].delegate window];
58
window.rootViewController = viewController;
59
}
60
61
- (UIStoryboard *)storyboardWithName:(NSString *)name {
62
return [UIStoryboard storyboardWithName: name bundle: [NSBundle mainBundle]];
63
}
64
65
# pragma mark User and session management
66
67
- (BOOL)isLoggedIn {
68
return [SessionManager isLoggedIn];
69
}
70
71
- (void)loginWithUsername:(NSString *)username
72
completion:(StatusWithErrorHandler)completion {
73
[SessionManager loginWithUsername:username];
74
[self connectClientWithCompletion:^(BOOL success, NSError *error) {
75
if (success) {
76
[self presentViewControllerByName:TWCMainViewControllerName];
77
}
78
completion(success, error);
79
}];
80
}
81
82
- (void)logout {
83
[SessionManager logout];
84
self.connected = NO;
85
86
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
87
[self.client shutdown];
88
self.client = nil;
89
});
90
}
91
92
# pragma mark Twilio client
93
94
- (void)connectClientWithCompletion:(StatusWithErrorHandler)completion {
95
if (self.client) {
96
[self logout];
97
}
98
99
[self requestTokenWithCompletion:^(BOOL succeeded, NSString *token) {
100
if (succeeded) {
101
[self initializeClientWithToken:token];
102
if (completion) completion(succeeded, nil);
103
}
104
else {
105
NSError *error = [self errorWithDescription:@"Could not get access token" code:301];
106
if (completion) completion(succeeded, error);
107
}
108
}];
109
}
110
111
- (void)initializeClientWithToken:(NSString *)token {
112
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
113
114
[TwilioChatClient chatClientWithToken:token
115
properties:nil
116
delegate:self
117
completion:^(TCHResult * _Nonnull result, TwilioChatClient * _Nullable chatClient) {
118
if (result.isSuccessful) {
119
self.client = chatClient;
120
self.connected = YES;
121
122
}
123
}];
124
}
125
126
- (void)requestTokenWithCompletion:(StatusWithTokenHandler)completion {
127
NSString *uuid = [[UIDevice currentDevice] identifierForVendor].UUIDString;
128
NSDictionary *parameters = @{@"device": uuid, @"identity": [SessionManager getUsername]};
129
130
[TokenRequestHandler fetchTokenWithParams:parameters completion:^(NSDictionary *results, NSError *error) {
131
NSString *token = [results objectForKey:TWCTokenKey];
132
BOOL errorCondition = error || !token;
133
134
if (completion) completion(!errorCondition, token);
135
}];
136
}
137
138
- (void)loadGeneralChatRoomWithCompletion:(StatusWithErrorHandler)completion {
139
[[ChannelManager sharedManager] joinGeneralChatRoomWithCompletion:^(BOOL succeeded) {
140
if (succeeded)
141
{
142
if (completion) completion(succeeded, nil);
143
}
144
else {
145
NSError *error = [self errorWithDescription:@"Could not join General channel" code:300];
146
if (completion) completion(succeeded, error);
147
}
148
}];
149
}
150
151
- (NSError *)errorWithDescription:(NSString *)description code:(NSInteger)code {
152
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};
153
NSError *error = [NSError errorWithDomain:@"app" code:code userInfo:userInfo];
154
return error;
155
}
156
157
#pragma mark Internal helpers
158
159
- (NSString *)userIdentity {
160
return [SessionManager getUsername];
161
}
162
163
- (void)refreshChatToken:(TwilioChatClient*)client {
164
[self requestTokenWithCompletion:^(BOOL succeeded, NSString *token) {
165
if (succeeded) {
166
[client updateToken:token completion:^(TCHResult * _Nonnull result) {
167
if (result.isSuccessful) {
168
169
}
170
}];
171
}
172
else {
173
NSLog(@"Error while trying to get new access token");
174
}
175
}];
176
}
177
178
#pragma mark TwilioChatClientDelegate
179
180
- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel {
181
[self.delegate chatClient:client channelAdded:channel];
182
}
183
184
- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {
185
[self.delegate chatClient:client channelDeleted:channel];
186
}
187
188
- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {
189
if (status == TCHClientSynchronizationStatusCompleted) {
190
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
191
[ChannelManager sharedManager].channelsList = client.channelsList;
192
[[ChannelManager sharedManager] populateChannels];
193
[self loadGeneralChatRoomWithCompletion:^(BOOL success, NSError *error) {
194
if (success) {
195
[self presentViewControllerByName:TWCMainViewControllerName];
196
}
197
}];
198
}
199
[self.delegate chatClient:client synchronizationStatusUpdated:status];
200
}
201
202
- (void)chatClientTokenWillExpire:(TwilioChatClient *)client {
203
[self refreshChatToken:client];
204
}
205
206
- (void)chatClientTokenExpired:(TwilioChatClient *)client {
207
[self refreshChatToken:client];
208
}
209
210
@end
211

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


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 TCHChannelList(link takes you to an external page) to our ChannelManager. Now we must get an actual array of channels using the userChannelsWithCompletion and publicChannelsWithCompletion methods.

twiliochat/ChannelManager.m

1
#import "ChannelManager.h"
2
#import "MessagingManager.h"
3
4
#define _ Underscore
5
6
@interface ChannelManager ()
7
@property (strong, nonatomic) TCHChannel *generalChannel;
8
@end
9
10
static NSString * const TWCDefaultChannelUniqueName = @"general";
11
static NSString * const TWCDefaultChannelName = @"General Channel";
12
13
static NSString * const TWCFriendlyNameKey = @"friendlyName";
14
15
@implementation ChannelManager
16
17
+ (instancetype)sharedManager {
18
static ChannelManager *sharedMyManager = nil;
19
static dispatch_once_t onceToken;
20
dispatch_once(&onceToken, ^{
21
sharedMyManager = [[self alloc] init];
22
});
23
return sharedMyManager;
24
}
25
26
- (instancetype)init {
27
self.channels = [[NSMutableOrderedSet alloc] init];
28
return self;
29
}
30
31
#pragma mark General channel
32
33
- (void)joinGeneralChatRoomWithCompletion:(SucceedHandler)completion {
34
[self.channelsList channelWithSidOrUniqueName:TWCDefaultChannelUniqueName completion:^(TCHResult *result, TCHChannel *channel) {
35
if ([result isSuccessful]) {
36
self.generalChannel = channel;
37
}
38
39
if (self.generalChannel) {
40
[self joinGeneralChatRoomWithUniqueName:nil completion:completion];
41
}
42
else {
43
[self createGeneralChatRoomWithCompletion:^(BOOL succeeded) {
44
if (succeeded) {
45
[self joinGeneralChatRoomWithUniqueName:TWCDefaultChannelUniqueName completion:completion];
46
return;
47
}
48
if (completion) completion(NO);
49
}];
50
};
51
}];
52
}
53
54
- (void)joinGeneralChatRoomWithUniqueName:(NSString *)uniqueName completion:(SucceedHandler)completion {
55
[self.generalChannel joinWithCompletion:^(TCHResult *result) {
56
if ([result isSuccessful]) {
57
if (uniqueName) {
58
[self setGeneralChatRoomUniqueNameWithCompletion:completion];
59
return;
60
}
61
}
62
if (completion) completion([result isSuccessful]);
63
}];
64
}
65
66
- (void)createGeneralChatRoomWithCompletion:(SucceedHandler)completion {
67
NSDictionary *options = [
68
NSDictionary
69
dictionaryWithObjectsAndKeys:TWCDefaultChannelName,
70
TCHChannelOptionFriendlyName,
71
TCHChannelTypePublic,
72
TCHChannelOptionType,
73
nil
74
];
75
76
[self.channelsList createChannelWithOptions:options
77
completion:^(TCHResult *result, TCHChannel *channel) {
78
if ([result isSuccessful]) {
79
self.generalChannel = channel;
80
}
81
if (completion) completion([result isSuccessful]);
82
}];
83
}
84
85
- (void)setGeneralChatRoomUniqueNameWithCompletion:(SucceedHandler)completion {
86
[self.generalChannel setUniqueName:TWCDefaultChannelUniqueName
87
completion:^(TCHResult *result) {
88
if (completion) completion([result isSuccessful]);
89
}];
90
}
91
92
#pragma mark Populate channels
93
94
- (void)populateChannels {
95
self.channels = [[NSMutableOrderedSet alloc] init];
96
[self.channelsList userChannelDescriptorsWithCompletion:^(TCHResult * _Nonnull result, TCHChannelDescriptorPaginator * _Nullable channelPaginator) {
97
[self.channels addObjectsFromArray:[channelPaginator items]];
98
[self sortAndDedupeChannels];
99
if (self.delegate) {
100
[self.delegate reloadChannelList];
101
}
102
}];
103
104
[self.channelsList publicChannelDescriptorsWithCompletion:^(TCHResult *result,
105
TCHChannelDescriptorPaginator *channelDescPaginator) {
106
[self.channels addObjectsFromArray: [channelDescPaginator items]];
107
[self sortAndDedupeChannels];
108
if (self.delegate) {
109
[self.delegate reloadChannelList];
110
}
111
}];
112
}
113
114
- (void)sortAndDedupeChannels {
115
NSMutableDictionary *channelsDict = [[NSMutableDictionary alloc] init];
116
117
for(TCHChannel *channel in self.channels) {
118
if (![channelsDict objectForKey: channel.sid] ||
119
![[channelsDict objectForKey: channel.sid] isKindOfClass: [NSNull class]]) {
120
[channelsDict setObject:channel forKey:channel.sid];
121
}
122
}
123
124
NSMutableOrderedSet *dedupedChannels = [NSMutableOrderedSet
125
orderedSetWithArray:[channelsDict allValues]];
126
127
SEL sortSelector = @selector(localizedCaseInsensitiveCompare:);
128
129
NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:TWCFriendlyNameKey
130
ascending:YES
131
selector:sortSelector];
132
133
[dedupedChannels sortUsingDescriptors:@[descriptor]];
134
135
self.channels = dedupedChannels;
136
}
137
138
# pragma mark Create channel
139
140
- (void)createChannelWithName:(NSString *)name completion:(ChannelHandler)completion {
141
if ([name isEqualToString:TWCDefaultChannelName]) {
142
if (completion) completion(NO, nil);
143
return;
144
}
145
146
NSDictionary *options = [
147
NSDictionary
148
dictionaryWithObjectsAndKeys:name,
149
TCHChannelOptionFriendlyName,
150
TCHChannelTypePublic,
151
TCHChannelOptionType,
152
nil
153
];
154
[self.channelsList
155
createChannelWithOptions:options
156
completion:^(TCHResult *result, TCHChannel *channel) {
157
[self.channels addObject:channel];
158
[self sortAndDedupeChannels];
159
if (completion) completion([result isSuccessful], channel);
160
}];
161
}
162
163
# pragma mark TwilioChatClientDelegate
164
165
- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel{
166
dispatch_async(dispatch_get_main_queue(), ^{
167
[self.channels addObject:channel];
168
[self sortAndDedupeChannels];
169
[self.delegate chatClient:client channelAdded:channel];
170
});
171
}
172
173
- (void)chatClient:(TwilioChatClient *)client channel:(nonnull TCHChannel *)channel updated:(TCHChannelUpdate)updated {
174
dispatch_async(dispatch_get_main_queue(), ^{
175
[self.delegate chatClient:client channel:channel updated:updated];
176
});
177
}
178
179
- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {
180
dispatch_async(dispatch_get_main_queue(), ^{
181
[[ChannelManager sharedManager].channels removeObject:channel];
182
[self.delegate chatClient:client channelDeleted:channel];
183
});
184
}
185
186
- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {
187
188
}
189
190
@end
191

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


The Chat Client will trigger events such as channelAdded 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(link takes you to an external page). 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.m

1
#import "ChannelManager.h"
2
#import "MessagingManager.h"
3
4
#define _ Underscore
5
6
@interface ChannelManager ()
7
@property (strong, nonatomic) TCHChannel *generalChannel;
8
@end
9
10
static NSString * const TWCDefaultChannelUniqueName = @"general";
11
static NSString * const TWCDefaultChannelName = @"General Channel";
12
13
static NSString * const TWCFriendlyNameKey = @"friendlyName";
14
15
@implementation ChannelManager
16
17
+ (instancetype)sharedManager {
18
static ChannelManager *sharedMyManager = nil;
19
static dispatch_once_t onceToken;
20
dispatch_once(&onceToken, ^{
21
sharedMyManager = [[self alloc] init];
22
});
23
return sharedMyManager;
24
}
25
26
- (instancetype)init {
27
self.channels = [[NSMutableOrderedSet alloc] init];
28
return self;
29
}
30
31
#pragma mark General channel
32
33
- (void)joinGeneralChatRoomWithCompletion:(SucceedHandler)completion {
34
[self.channelsList channelWithSidOrUniqueName:TWCDefaultChannelUniqueName completion:^(TCHResult *result, TCHChannel *channel) {
35
if ([result isSuccessful]) {
36
self.generalChannel = channel;
37
}
38
39
if (self.generalChannel) {
40
[self joinGeneralChatRoomWithUniqueName:nil completion:completion];
41
}
42
else {
43
[self createGeneralChatRoomWithCompletion:^(BOOL succeeded) {
44
if (succeeded) {
45
[self joinGeneralChatRoomWithUniqueName:TWCDefaultChannelUniqueName completion:completion];
46
return;
47
}
48
if (completion) completion(NO);
49
}];
50
};
51
}];
52
}
53
54
- (void)joinGeneralChatRoomWithUniqueName:(NSString *)uniqueName completion:(SucceedHandler)completion {
55
[self.generalChannel joinWithCompletion:^(TCHResult *result) {
56
if ([result isSuccessful]) {
57
if (uniqueName) {
58
[self setGeneralChatRoomUniqueNameWithCompletion:completion];
59
return;
60
}
61
}
62
if (completion) completion([result isSuccessful]);
63
}];
64
}
65
66
- (void)createGeneralChatRoomWithCompletion:(SucceedHandler)completion {
67
NSDictionary *options = [
68
NSDictionary
69
dictionaryWithObjectsAndKeys:TWCDefaultChannelName,
70
TCHChannelOptionFriendlyName,
71
TCHChannelTypePublic,
72
TCHChannelOptionType,
73
nil
74
];
75
76
[self.channelsList createChannelWithOptions:options
77
completion:^(TCHResult *result, TCHChannel *channel) {
78
if ([result isSuccessful]) {
79
self.generalChannel = channel;
80
}
81
if (completion) completion([result isSuccessful]);
82
}];
83
}
84
85
- (void)setGeneralChatRoomUniqueNameWithCompletion:(SucceedHandler)completion {
86
[self.generalChannel setUniqueName:TWCDefaultChannelUniqueName
87
completion:^(TCHResult *result) {
88
if (completion) completion([result isSuccessful]);
89
}];
90
}
91
92
#pragma mark Populate channels
93
94
- (void)populateChannels {
95
self.channels = [[NSMutableOrderedSet alloc] init];
96
[self.channelsList userChannelDescriptorsWithCompletion:^(TCHResult * _Nonnull result, TCHChannelDescriptorPaginator * _Nullable channelPaginator) {
97
[self.channels addObjectsFromArray:[channelPaginator items]];
98
[self sortAndDedupeChannels];
99
if (self.delegate) {
100
[self.delegate reloadChannelList];
101
}
102
}];
103
104
[self.channelsList publicChannelDescriptorsWithCompletion:^(TCHResult *result,
105
TCHChannelDescriptorPaginator *channelDescPaginator) {
106
[self.channels addObjectsFromArray: [channelDescPaginator items]];
107
[self sortAndDedupeChannels];
108
if (self.delegate) {
109
[self.delegate reloadChannelList];
110
}
111
}];
112
}
113
114
- (void)sortAndDedupeChannels {
115
NSMutableDictionary *channelsDict = [[NSMutableDictionary alloc] init];
116
117
for(TCHChannel *channel in self.channels) {
118
if (![channelsDict objectForKey: channel.sid] ||
119
![[channelsDict objectForKey: channel.sid] isKindOfClass: [NSNull class]]) {
120
[channelsDict setObject:channel forKey:channel.sid];
121
}
122
}
123
124
NSMutableOrderedSet *dedupedChannels = [NSMutableOrderedSet
125
orderedSetWithArray:[channelsDict allValues]];
126
127
SEL sortSelector = @selector(localizedCaseInsensitiveCompare:);
128
129
NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:TWCFriendlyNameKey
130
ascending:YES
131
selector:sortSelector];
132
133
[dedupedChannels sortUsingDescriptors:@[descriptor]];
134
135
self.channels = dedupedChannels;
136
}
137
138
# pragma mark Create channel
139
140
- (void)createChannelWithName:(NSString *)name completion:(ChannelHandler)completion {
141
if ([name isEqualToString:TWCDefaultChannelName]) {
142
if (completion) completion(NO, nil);
143
return;
144
}
145
146
NSDictionary *options = [
147
NSDictionary
148
dictionaryWithObjectsAndKeys:name,
149
TCHChannelOptionFriendlyName,
150
TCHChannelTypePublic,
151
TCHChannelOptionType,
152
nil
153
];
154
[self.channelsList
155
createChannelWithOptions:options
156
completion:^(TCHResult *result, TCHChannel *channel) {
157
[self.channels addObject:channel];
158
[self sortAndDedupeChannels];
159
if (completion) completion([result isSuccessful], channel);
160
}];
161
}
162
163
# pragma mark TwilioChatClientDelegate
164
165
- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel{
166
dispatch_async(dispatch_get_main_queue(), ^{
167
[self.channels addObject:channel];
168
[self sortAndDedupeChannels];
169
[self.delegate chatClient:client channelAdded:channel];
170
});
171
}
172
173
- (void)chatClient:(TwilioChatClient *)client channel:(nonnull TCHChannel *)channel updated:(TCHChannelUpdate)updated {
174
dispatch_async(dispatch_get_main_queue(), ^{
175
[self.delegate chatClient:client channel:channel updated:updated];
176
});
177
}
178
179
- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {
180
dispatch_async(dispatch_get_main_queue(), ^{
181
[[ChannelManager sharedManager].channels removeObject:channel];
182
[self.delegate chatClient:client channelDeleted:channel];
183
});
184
}
185
186
- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {
187
188
}
189
190
@end
191

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 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.m

1
#import "ChannelManager.h"
2
#import "MessagingManager.h"
3
4
#define _ Underscore
5
6
@interface ChannelManager ()
7
@property (strong, nonatomic) TCHChannel *generalChannel;
8
@end
9
10
static NSString * const TWCDefaultChannelUniqueName = @"general";
11
static NSString * const TWCDefaultChannelName = @"General Channel";
12
13
static NSString * const TWCFriendlyNameKey = @"friendlyName";
14
15
@implementation ChannelManager
16
17
+ (instancetype)sharedManager {
18
static ChannelManager *sharedMyManager = nil;
19
static dispatch_once_t onceToken;
20
dispatch_once(&onceToken, ^{
21
sharedMyManager = [[self alloc] init];
22
});
23
return sharedMyManager;
24
}
25
26
- (instancetype)init {
27
self.channels = [[NSMutableOrderedSet alloc] init];
28
return self;
29
}
30
31
#pragma mark General channel
32
33
- (void)joinGeneralChatRoomWithCompletion:(SucceedHandler)completion {
34
[self.channelsList channelWithSidOrUniqueName:TWCDefaultChannelUniqueName completion:^(TCHResult *result, TCHChannel *channel) {
35
if ([result isSuccessful]) {
36
self.generalChannel = channel;
37
}
38
39
if (self.generalChannel) {
40
[self joinGeneralChatRoomWithUniqueName:nil completion:completion];
41
}
42
else {
43
[self createGeneralChatRoomWithCompletion:^(BOOL succeeded) {
44
if (succeeded) {
45
[self joinGeneralChatRoomWithUniqueName:TWCDefaultChannelUniqueName completion:completion];
46
return;
47
}
48
if (completion) completion(NO);
49
}];
50
};
51
}];
52
}
53
54
- (void)joinGeneralChatRoomWithUniqueName:(NSString *)uniqueName completion:(SucceedHandler)completion {
55
[self.generalChannel joinWithCompletion:^(TCHResult *result) {
56
if ([result isSuccessful]) {
57
if (uniqueName) {
58
[self setGeneralChatRoomUniqueNameWithCompletion:completion];
59
return;
60
}
61
}
62
if (completion) completion([result isSuccessful]);
63
}];
64
}
65
66
- (void)createGeneralChatRoomWithCompletion:(SucceedHandler)completion {
67
NSDictionary *options = [
68
NSDictionary
69
dictionaryWithObjectsAndKeys:TWCDefaultChannelName,
70
TCHChannelOptionFriendlyName,
71
TCHChannelTypePublic,
72
TCHChannelOptionType,
73
nil
74
];
75
76
[self.channelsList createChannelWithOptions:options
77
completion:^(TCHResult *result, TCHChannel *channel) {
78
if ([result isSuccessful]) {
79
self.generalChannel = channel;
80
}
81
if (completion) completion([result isSuccessful]);
82
}];
83
}
84
85
- (void)setGeneralChatRoomUniqueNameWithCompletion:(SucceedHandler)completion {
86
[self.generalChannel setUniqueName:TWCDefaultChannelUniqueName
87
completion:^(TCHResult *result) {
88
if (completion) completion([result isSuccessful]);
89
}];
90
}
91
92
#pragma mark Populate channels
93
94
- (void)populateChannels {
95
self.channels = [[NSMutableOrderedSet alloc] init];
96
[self.channelsList userChannelDescriptorsWithCompletion:^(TCHResult * _Nonnull result, TCHChannelDescriptorPaginator * _Nullable channelPaginator) {
97
[self.channels addObjectsFromArray:[channelPaginator items]];
98
[self sortAndDedupeChannels];
99
if (self.delegate) {
100
[self.delegate reloadChannelList];
101
}
102
}];
103
104
[self.channelsList publicChannelDescriptorsWithCompletion:^(TCHResult *result,
105
TCHChannelDescriptorPaginator *channelDescPaginator) {
106
[self.channels addObjectsFromArray: [channelDescPaginator items]];
107
[self sortAndDedupeChannels];
108
if (self.delegate) {
109
[self.delegate reloadChannelList];
110
}
111
}];
112
}
113
114
- (void)sortAndDedupeChannels {
115
NSMutableDictionary *channelsDict = [[NSMutableDictionary alloc] init];
116
117
for(TCHChannel *channel in self.channels) {
118
if (![channelsDict objectForKey: channel.sid] ||
119
![[channelsDict objectForKey: channel.sid] isKindOfClass: [NSNull class]]) {
120
[channelsDict setObject:channel forKey:channel.sid];
121
}
122
}
123
124
NSMutableOrderedSet *dedupedChannels = [NSMutableOrderedSet
125
orderedSetWithArray:[channelsDict allValues]];
126
127
SEL sortSelector = @selector(localizedCaseInsensitiveCompare:);
128
129
NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:TWCFriendlyNameKey
130
ascending:YES
131
selector:sortSelector];
132
133
[dedupedChannels sortUsingDescriptors:@[descriptor]];
134
135
self.channels = dedupedChannels;
136
}
137
138
# pragma mark Create channel
139
140
- (void)createChannelWithName:(NSString *)name completion:(ChannelHandler)completion {
141
if ([name isEqualToString:TWCDefaultChannelName]) {
142
if (completion) completion(NO, nil);
143
return;
144
}
145
146
NSDictionary *options = [
147
NSDictionary
148
dictionaryWithObjectsAndKeys:name,
149
TCHChannelOptionFriendlyName,
150
TCHChannelTypePublic,
151
TCHChannelOptionType,
152
nil
153
];
154
[self.channelsList
155
createChannelWithOptions:options
156
completion:^(TCHResult *result, TCHChannel *channel) {
157
[self.channels addObject:channel];
158
[self sortAndDedupeChannels];
159
if (completion) completion([result isSuccessful], channel);
160
}];
161
}
162
163
# pragma mark TwilioChatClientDelegate
164
165
- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel{
166
dispatch_async(dispatch_get_main_queue(), ^{
167
[self.channels addObject:channel];
168
[self sortAndDedupeChannels];
169
[self.delegate chatClient:client channelAdded:channel];
170
});
171
}
172
173
- (void)chatClient:(TwilioChatClient *)client channel:(nonnull TCHChannel *)channel updated:(TCHChannelUpdate)updated {
174
dispatch_async(dispatch_get_main_queue(), ^{
175
[self.delegate chatClient:client channel:channel updated:updated];
176
});
177
}
178
179
- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {
180
dispatch_async(dispatch_get_main_queue(), ^{
181
[[ChannelManager sharedManager].channels removeObject:channel];
182
[self.delegate chatClient:client channelDeleted:channel];
183
});
184
}
185
186
- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {
187
188
}
189
190
@end
191

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:

  • channelDeleted: When someone deletes a channel.
  • memberJoined: When someone joins the channel.
  • memberLeft: When someone leaves the channel.
  • messageAdded: When someone sends a message to the channel you are connected to.
  • 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.m

1
#import <TwilioChatClient/TwilioChatClient.h>
2
#import "MainChatViewController.h"
3
#import "ChatTableCell.h"
4
#import "NSDate+ISO8601Parser.h"
5
#import "SWRevealViewController.h"
6
#import "ChannelManager.h"
7
#import "StatusEntry.h"
8
#import "DateTodayFormatter.h"
9
#import "MenuViewController.h"
10
11
@interface MainChatViewController ()
12
@property (weak, nonatomic) IBOutlet UIBarButtonItem *revealButtonItem;
13
@property (weak, nonatomic) IBOutlet UIBarButtonItem *actionButtonItem;
14
15
@property (strong, nonatomic) NSMutableOrderedSet *messages;
16
17
@end
18
19
static NSString * const TWCChatCellIdentifier = @"ChatTableCell";
20
static NSString * const TWCChatStatusCellIdentifier = @"ChatStatusTableCell";
21
22
static NSString * const TWCOpenGeneralChannelSegue = @"OpenGeneralChat";
23
static NSInteger const TWCLabelTag = 200;
24
25
@implementation MainChatViewController
26
27
#pragma mark Initialization
28
29
- (void)viewDidLoad {
30
[super viewDidLoad];
31
32
if (self.revealViewController)
33
{
34
[self.revealButtonItem setTarget: self.revealViewController];
35
[self.revealButtonItem setAction: @selector( revealToggle: )];
36
[self.navigationController.navigationBar addGestureRecognizer: self.revealViewController.panGestureRecognizer];
37
self.revealViewController.rearViewRevealOverdraw = 0.f;
38
}
39
40
self.bounces = YES;
41
self.shakeToClearEnabled = YES;
42
self.keyboardPanningEnabled = YES;
43
self.shouldScrollToBottomAfterKeyboardShows = NO;
44
self.inverted = YES;
45
46
UINib *cellNib = [UINib nibWithNibName:TWCChatCellIdentifier bundle:nil];
47
[self.tableView registerNib:cellNib
48
forCellReuseIdentifier:TWCChatCellIdentifier];
49
50
UINib *cellStatusNib = [UINib nibWithNibName:TWCChatStatusCellIdentifier bundle:nil];
51
[self.tableView registerNib:cellStatusNib
52
forCellReuseIdentifier:TWCChatStatusCellIdentifier];
53
54
self.textInputbar.autoHideRightButton = YES;
55
self.textInputbar.maxCharCount = 256;
56
self.textInputbar.counterStyle = SLKCounterStyleSplit;
57
self.textInputbar.counterPosition = SLKCounterPositionTop;
58
59
UIFont *font = [UIFont fontWithName:@"Avenir-Light" size:14];
60
self.textView.font = font;
61
62
[self.rightButton setTitleColor:[UIColor colorWithRed:0.973 green:0.557 blue:0.502 alpha:1]
63
forState:UIControlStateNormal];
64
65
font = [UIFont fontWithName:@"Avenir-Heavy" size:17];
66
self.navigationController.navigationBar.titleTextAttributes = @{NSFontAttributeName:font};
67
68
self.tableView.allowsSelection = NO;
69
self.tableView.estimatedRowHeight = 70;
70
self.tableView.rowHeight = UITableViewAutomaticDimension;
71
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
72
73
if (!self.channel) {
74
id generalChannel = [ChannelManager sharedManager].generalChannel;
75
if (generalChannel) {
76
self.channel = generalChannel;
77
} else {
78
[[ChannelManager sharedManager] joinGeneralChatRoomWithCompletion:^(BOOL succeeded) {
79
if (succeeded) {
80
self.channel = [ChannelManager sharedManager].generalChannel;
81
}
82
}];
83
}
84
}
85
}
86
87
- (void)viewDidLayoutSubviews {
88
[super viewDidLayoutSubviews];
89
[self.textInputbar bringSubviewToFront:self.textInputbar.textView];
90
[self.textInputbar bringSubviewToFront:self.textInputbar.leftButton];
91
[self.textInputbar bringSubviewToFront:self.textInputbar.rightButton];
92
}
93
94
- (void)viewDidAppear:(BOOL)animated {
95
[super viewDidAppear:animated];
96
[self scrollToBottomMessage];
97
}
98
99
- (NSMutableOrderedSet *)messages {
100
if (!_messages) {
101
_messages = [[NSMutableOrderedSet alloc] init];
102
}
103
return _messages;
104
}
105
106
- (void)setChannel:(TCHChannel *)channel {
107
if ([channel isKindOfClass:[TCHChannelDescriptor class]]) {
108
TCHChannelDescriptor *channelDescriptor = (TCHChannelDescriptor*)channel;
109
[channelDescriptor channelWithCompletion:^(TCHResult *success, TCHChannel *channel) {
110
if (success) {
111
[self actuallySetChannel:channel];
112
}
113
}];
114
} else {
115
[self actuallySetChannel:channel];
116
}
117
}
118
119
- (void)actuallySetChannel:(TCHChannel *)channel {
120
_channel = channel;
121
self.title = self.channel.friendlyName;
122
self.channel.delegate = self;
123
124
if (self.channel == [ChannelManager sharedManager].generalChannel) {
125
self.navigationItem.rightBarButtonItem = nil;
126
}
127
128
[self setViewOnHold:YES];
129
130
if (self.channel.status != TCHChannelStatusJoined) {
131
[self.channel joinWithCompletion:^(TCHResult* result) {
132
NSLog(@"%@", @"Channel Joined");
133
[self setViewOnHold:NO];
134
}];
135
}
136
if (self.channel.synchronizationStatus == TCHChannelSynchronizationStatusAll) {
137
[self loadMessages];
138
[self setViewOnHold:NO];
139
}
140
}
141
142
// Disable user input and show activity indicator
143
- (void)setViewOnHold:(BOOL)onHold {
144
self.textInputbarHidden = onHold;
145
[UIApplication sharedApplication].networkActivityIndicatorVisible = onHold;
146
}
147
148
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
149
return 1;
150
}
151
152
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
153
return self.messages.count;
154
}
155
156
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
157
UITableViewCell *cell = nil;
158
159
id message = [self.messages objectAtIndex:indexPath.row];
160
161
if ([message isKindOfClass:[TCHMessage class]]) {
162
cell = [self getChatCellForTableView:tableView forIndexPath:indexPath message:message];
163
}
164
else {
165
cell = [self getStatusCellForTableView:tableView forIndexPath:indexPath message:message];
166
}
167
168
cell.transform = tableView.transform;
169
return cell;
170
}
171
172
- (ChatTableCell *)getChatCellForTableView:(UITableView *)tableView
173
forIndexPath:(NSIndexPath *)indexPath
174
message:(TCHMessage *)message {
175
UITableViewCell *cell = [self.tableView
176
dequeueReusableCellWithIdentifier:TWCChatCellIdentifier forIndexPath:indexPath];
177
178
ChatTableCell *chatCell = (ChatTableCell *)cell;
179
chatCell.user = message.author;
180
chatCell.date = [[[DateTodayFormatter alloc] init]
181
stringFromDate:[NSDate dateWithISO8601String:message.timestamp]];
182
183
chatCell.message = message.body;
184
185
return chatCell;
186
}
187
188
- (UITableViewCell *)getStatusCellForTableView:(UITableView *)tableView
189
forIndexPath:(NSIndexPath *)indexPath
190
message:(StatusEntry *)message {
191
UITableViewCell *cell = [self.tableView
192
dequeueReusableCellWithIdentifier:TWCChatStatusCellIdentifier forIndexPath:indexPath];
193
194
UILabel *label = [cell viewWithTag:TWCLabelTag];
195
label.text = [NSString stringWithFormat:@"User %@ has %@",
196
message.member.identity, (message.status == TWCMemberStatusJoined) ? @"joined" : @"left"];
197
198
return cell;
199
}
200
201
- (void)didPressRightButton:(id)sender {
202
[self.textView refreshFirstResponder];
203
[self sendMessage: [self.textView.text copy]];
204
[super didPressRightButton:sender];
205
}
206
207
#pragma mark Chat Service
208
- (void)sendMessage: (NSString *)inputMessage {
209
TCHMessageOptions *messageOptions = [[[TCHMessageOptions alloc] init] withBody:inputMessage];
210
[self.channel.messages sendMessageWithOptions:messageOptions
211
completion:nil];
212
}
213
214
215
216
- (void)addMessages:(NSArray *)messages {
217
[self.messages addObjectsFromArray:messages];
218
[self sortMessages];
219
dispatch_async(dispatch_get_main_queue(), ^{
220
[self.tableView reloadData];
221
if (self.messages.count > 0) {
222
[self scrollToBottomMessage];
223
}
224
});
225
}
226
227
228
- (void)sortMessages {
229
[self.messages sortUsingDescriptors:@[[[NSSortDescriptor alloc]
230
initWithKey:@"timestamp" ascending:NO]]];
231
}
232
233
- (void)scrollToBottomMessage {
234
if (self.messages.count == 0) {
235
return;
236
}
237
238
NSIndexPath *bottomMessageIndex = [NSIndexPath indexPathForRow:0
239
inSection:0];
240
[self.tableView scrollToRowAtIndexPath:bottomMessageIndex
241
atScrollPosition:UITableViewScrollPositionBottom animated:NO];
242
}
243
244
- (void)loadMessages {
245
[self.messages removeAllObjects];
246
if (self.channel.synchronizationStatus == TCHChannelSynchronizationStatusAll) {
247
[self.channel.messages
248
getLastMessagesWithCount:100
249
completion:^(TCHResult *result, NSArray *messages) {
250
if ([result isSuccessful]) {
251
[self addMessages: messages];
252
}
253
}];
254
}
255
}
256
257
- (void)leaveChannel {
258
[self.channel leaveWithCompletion:^(TCHResult* result) {
259
if ([result isSuccessful]) {
260
[(MenuViewController *)self.revealViewController.rearViewController deselectSelectedChannel];
261
[self.revealViewController.rearViewController
262
performSegueWithIdentifier:TWCOpenGeneralChannelSegue sender:nil];
263
}
264
}];
265
}
266
267
#pragma mark - TMMessageDelegate
268
269
- (void)chatClient:(TwilioChatClient *)client
270
channel:(TCHChannel *)channel
271
messageAdded:(TCHMessage *)message {
272
if (![self.messages containsObject:message]) {
273
[self addMessages:@[message]];
274
}
275
}
276
277
- (void)chatClient:(TwilioChatClient *)client
278
channelDeleted:(TCHChannel *)channel {
279
dispatch_async(dispatch_get_main_queue(), ^{
280
if (channel == self.channel) {
281
[self.revealViewController.rearViewController
282
performSegueWithIdentifier:TWCOpenGeneralChannelSegue sender:nil];
283
}
284
});
285
}
286
287
- (void)chatClient:(TwilioChatClient *)client
288
channel:(TCHChannel *)channel
289
memberJoined:(TCHMember *)member {
290
[self addMessages:@[[StatusEntry statusEntryWithMember:member status:TWCMemberStatusJoined]]];
291
}
292
293
- (void)chatClient:(TwilioChatClient *)client
294
channel:(TCHChannel *)channel
295
memberLeft:(TCHMember *)member {
296
[self addMessages:@[[StatusEntry statusEntryWithMember:member status:TWCMemberStatusLeft]]];
297
}
298
299
- (void)chatClient:(TwilioChatClient *)client channel:(TCHChannel *)channel synchronizationStatusChanged:(TCHChannelSynchronizationStatus)status {
300
if (status == TCHChannelSynchronizationStatusAll) {
301
[self loadMessages];
302
dispatch_async(dispatch_get_main_queue(), ^{
303
[self.tableView reloadData];
304
[self setViewOnHold:NO];
305
});
306
}
307
}
308
309
#pragma mark - Actions
310
311
- (IBAction)actionButtonTouched:(UIBarButtonItem *)sender {
312
[self leaveChannel];
313
}
314
315
- (IBAction)revealButtonTouched:(UIBarButtonItem *)sender {
316
[self.revealViewController revealToggleAnimated:YES];
317
}
318
319
@end
320

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 setChannel method takes care of joining to the selected channel and loading the messages.

twiliochat/MainChatViewController.m

1
#import <TwilioChatClient/TwilioChatClient.h>
2
#import "MainChatViewController.h"
3
#import "ChatTableCell.h"
4
#import "NSDate+ISO8601Parser.h"
5
#import "SWRevealViewController.h"
6
#import "ChannelManager.h"
7
#import "StatusEntry.h"
8
#import "DateTodayFormatter.h"
9
#import "MenuViewController.h"
10
11
@interface MainChatViewController ()
12
@property (weak, nonatomic) IBOutlet UIBarButtonItem *revealButtonItem;
13
@property (weak, nonatomic) IBOutlet UIBarButtonItem *actionButtonItem;
14
15
@property (strong, nonatomic) NSMutableOrderedSet *messages;
16
17
@end
18
19
static NSString * const TWCChatCellIdentifier = @"ChatTableCell";
20
static NSString * const TWCChatStatusCellIdentifier = @"ChatStatusTableCell";
21
22
static NSString * const TWCOpenGeneralChannelSegue = @"OpenGeneralChat";
23
static NSInteger const TWCLabelTag = 200;
24
25
@implementation MainChatViewController
26
27
#pragma mark Initialization
28
29
- (void)viewDidLoad {
30
[super viewDidLoad];
31
32
if (self.revealViewController)
33
{
34
[self.revealButtonItem setTarget: self.revealViewController];
35
[self.revealButtonItem setAction: @selector( revealToggle: )];
36
[self.navigationController.navigationBar addGestureRecognizer: self.revealViewController.panGestureRecognizer];
37
self.revealViewController.rearViewRevealOverdraw = 0.f;
38
}
39
40
self.bounces = YES;
41
self.shakeToClearEnabled = YES;
42
self.keyboardPanningEnabled = YES;
43
self.shouldScrollToBottomAfterKeyboardShows = NO;
44
self.inverted = YES;
45
46
UINib *cellNib = [UINib nibWithNibName:TWCChatCellIdentifier bundle:nil];
47
[self.tableView registerNib:cellNib
48
forCellReuseIdentifier:TWCChatCellIdentifier];
49
50
UINib *cellStatusNib = [UINib nibWithNibName:TWCChatStatusCellIdentifier bundle:nil];
51
[self.tableView registerNib:cellStatusNib
52
forCellReuseIdentifier:TWCChatStatusCellIdentifier];
53
54
self.textInputbar.autoHideRightButton = YES;
55
self.textInputbar.maxCharCount = 256;
56
self.textInputbar.counterStyle = SLKCounterStyleSplit;
57
self.textInputbar.counterPosition = SLKCounterPositionTop;
58
59
UIFont *font = [UIFont fontWithName:@"Avenir-Light" size:14];
60
self.textView.font = font;
61
62
[self.rightButton setTitleColor:[UIColor colorWithRed:0.973 green:0.557 blue:0.502 alpha:1]
63
forState:UIControlStateNormal];
64
65
font = [UIFont fontWithName:@"Avenir-Heavy" size:17];
66
self.navigationController.navigationBar.titleTextAttributes = @{NSFontAttributeName:font};
67
68
self.tableView.allowsSelection = NO;
69
self.tableView.estimatedRowHeight = 70;
70
self.tableView.rowHeight = UITableViewAutomaticDimension;
71
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
72
73
if (!self.channel) {
74
id generalChannel = [ChannelManager sharedManager].generalChannel;
75
if (generalChannel) {
76
self.channel = generalChannel;
77
} else {
78
[[ChannelManager sharedManager] joinGeneralChatRoomWithCompletion:^(BOOL succeeded) {
79
if (succeeded) {
80
self.channel = [ChannelManager sharedManager].generalChannel;
81
}
82
}];
83
}
84
}
85
}
86
87
- (void)viewDidLayoutSubviews {
88
[super viewDidLayoutSubviews];
89
[self.textInputbar bringSubviewToFront:self.textInputbar.textView];
90
[self.textInputbar bringSubviewToFront:self.textInputbar.leftButton];
91
[self.textInputbar bringSubviewToFront:self.textInputbar.rightButton];
92
}
93
94
- (void)viewDidAppear:(BOOL)animated {
95
[super viewDidAppear:animated];
96
[self scrollToBottomMessage];
97
}
98
99
- (NSMutableOrderedSet *)messages {
100
if (!_messages) {
101
_messages = [[NSMutableOrderedSet alloc] init];
102
}
103
return _messages;
104
}
105
106
- (void)setChannel:(TCHChannel *)channel {
107
if ([channel isKindOfClass:[TCHChannelDescriptor class]]) {
108
TCHChannelDescriptor *channelDescriptor = (TCHChannelDescriptor*)channel;
109
[channelDescriptor channelWithCompletion:^(TCHResult *success, TCHChannel *channel) {
110
if (success) {
111
[self actuallySetChannel:channel];
112
}
113
}];
114
} else {
115
[self actuallySetChannel:channel];
116
}
117
}
118
119
- (void)actuallySetChannel:(TCHChannel *)channel {
120
_channel = channel;
121
self.title = self.channel.friendlyName;
122
self.channel.delegate = self;
123
124
if (self.channel == [ChannelManager sharedManager].generalChannel) {
125
self.navigationItem.rightBarButtonItem = nil;
126
}
127
128
[self setViewOnHold:YES];
129
130
if (self.channel.status != TCHChannelStatusJoined) {
131
[self.channel joinWithCompletion:^(TCHResult* result) {
132
NSLog(@"%@", @"Channel Joined");
133
[self setViewOnHold:NO];
134
}];
135
}
136
if (self.channel.synchronizationStatus == TCHChannelSynchronizationStatusAll) {
137
[self loadMessages];
138
[self setViewOnHold:NO];
139
}
140
}
141
142
// Disable user input and show activity indicator
143
- (void)setViewOnHold:(BOOL)onHold {
144
self.textInputbarHidden = onHold;
145
[UIApplication sharedApplication].networkActivityIndicatorVisible = onHold;
146
}
147
148
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
149
return 1;
150
}
151
152
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
153
return self.messages.count;
154
}
155
156
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
157
UITableViewCell *cell = nil;
158
159
id message = [self.messages objectAtIndex:indexPath.row];
160
161
if ([message isKindOfClass:[TCHMessage class]]) {
162
cell = [self getChatCellForTableView:tableView forIndexPath:indexPath message:message];
163
}
164
else {
165
cell = [self getStatusCellForTableView:tableView forIndexPath:indexPath message:message];
166
}
167
168
cell.transform = tableView.transform;
169
return cell;
170
}
171
172
- (ChatTableCell *)getChatCellForTableView:(UITableView *)tableView
173
forIndexPath:(NSIndexPath *)indexPath
174
message:(TCHMessage *)message {
175
UITableViewCell *cell = [self.tableView
176
dequeueReusableCellWithIdentifier:TWCChatCellIdentifier forIndexPath:indexPath];
177
178
ChatTableCell *chatCell = (ChatTableCell *)cell;
179
chatCell.user = message.author;
180
chatCell.date = [[[DateTodayFormatter alloc] init]
181
stringFromDate:[NSDate dateWithISO8601String:message.timestamp]];
182
183
chatCell.message = message.body;
184
185
return chatCell;
186
}
187
188
- (UITableViewCell *)getStatusCellForTableView:(UITableView *)tableView
189
forIndexPath:(NSIndexPath *)indexPath
190
message:(StatusEntry *)message {
191
UITableViewCell *cell = [self.tableView
192
dequeueReusableCellWithIdentifier:TWCChatStatusCellIdentifier forIndexPath:indexPath];
193
194
UILabel *label = [cell viewWithTag:TWCLabelTag];
195
label.text = [NSString stringWithFormat:@"User %@ has %@",
196
message.member.identity, (message.status == TWCMemberStatusJoined) ? @"joined" : @"left"];
197
198
return cell;
199
}
200
201
- (void)didPressRightButton:(id)sender {
202
[self.textView refreshFirstResponder];
203
[self sendMessage: [self.textView.text copy]];
204
[super didPressRightButton:sender];
205
}
206
207
#pragma mark Chat Service
208
- (void)sendMessage: (NSString *)inputMessage {
209
TCHMessageOptions *messageOptions = [[[TCHMessageOptions alloc] init] withBody:inputMessage];
210
[self.channel.messages sendMessageWithOptions:messageOptions
211
completion:nil];
212
}
213
214
215
216
- (void)addMessages:(NSArray *)messages {
217
[self.messages addObjectsFromArray:messages];
218
[self sortMessages];
219
dispatch_async(dispatch_get_main_queue(), ^{
220
[self.tableView reloadData];
221
if (self.messages.count > 0) {
222
[self scrollToBottomMessage];
223
}
224
});
225
}
226
227
228
- (void)sortMessages {
229
[self.messages sortUsingDescriptors:@[[[NSSortDescriptor alloc]
230
initWithKey:@"timestamp" ascending:NO]]];
231
}
232
233
- (void)scrollToBottomMessage {
234
if (self.messages.count == 0) {
235
return;
236
}
237
238
NSIndexPath *bottomMessageIndex = [NSIndexPath indexPathForRow:0
239
inSection:0];
240
[self.tableView scrollToRowAtIndexPath:bottomMessageIndex
241
atScrollPosition:UITableViewScrollPositionBottom animated:NO];
242
}
243
244
- (void)loadMessages {
245
[self.messages removeAllObjects];
246
if (self.channel.synchronizationStatus == TCHChannelSynchronizationStatusAll) {
247
[self.channel.messages
248
getLastMessagesWithCount:100
249
completion:^(TCHResult *result, NSArray *messages) {
250
if ([result isSuccessful]) {
251
[self addMessages: messages];
252
}
253
}];
254
}
255
}
256
257
- (void)leaveChannel {
258
[self.channel leaveWithCompletion:^(TCHResult* result) {
259
if ([result isSuccessful]) {
260
[(MenuViewController *)self.revealViewController.rearViewController deselectSelectedChannel];
261
[self.revealViewController.rearViewController
262
performSegueWithIdentifier:TWCOpenGeneralChannelSegue sender:nil];
263
}
264
}];
265
}
266
267
#pragma mark - TMMessageDelegate
268
269
- (void)chatClient:(TwilioChatClient *)client
270
channel:(TCHChannel *)channel
271
messageAdded:(TCHMessage *)message {
272
if (![self.messages containsObject:message]) {
273
[self addMessages:@[message]];
274
}
275
}
276
277
- (void)chatClient:(TwilioChatClient *)client
278
channelDeleted:(TCHChannel *)channel {
279
dispatch_async(dispatch_get_main_queue(), ^{
280
if (channel == self.channel) {
281
[self.revealViewController.rearViewController
282
performSegueWithIdentifier:TWCOpenGeneralChannelSegue sender:nil];
283
}
284
});
285
}
286
287
- (void)chatClient:(TwilioChatClient *)client
288
channel:(TCHChannel *)channel
289
memberJoined:(TCHMember *)member {
290
[self addMessages:@[[StatusEntry statusEntryWithMember:member status:TWCMemberStatusJoined]]];
291
}
292
293
- (void)chatClient:(TwilioChatClient *)client
294
channel:(TCHChannel *)channel
295
memberLeft:(TCHMember *)member {
296
[self addMessages:@[[StatusEntry statusEntryWithMember:member status:TWCMemberStatusLeft]]];
297
}
298
299
- (void)chatClient:(TwilioChatClient *)client channel:(TCHChannel *)channel synchronizationStatusChanged:(TCHChannelSynchronizationStatus)status {
300
if (status == TCHChannelSynchronizationStatusAll) {
301
[self loadMessages];
302
dispatch_async(dispatch_get_main_queue(), ^{
303
[self.tableView reloadData];
304
[self setViewOnHold:NO];
305
});
306
}
307
}
308
309
#pragma mark - Actions
310
311
- (IBAction)actionButtonTouched:(UIBarButtonItem *)sender {
312
[self leaveChannel];
313
}
314
315
- (IBAction)revealButtonTouched:(UIBarButtonItem *)sender {
316
[self.revealViewController revealToggleAnimated:YES];
317
}
318
319
@end
320

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 createChannelWithOptions and passing a dictionary with the new channel information.

twiliochat/ChannelManager.m

1
#import "ChannelManager.h"
2
#import "MessagingManager.h"
3
4
#define _ Underscore
5
6
@interface ChannelManager ()
7
@property (strong, nonatomic) TCHChannel *generalChannel;
8
@end
9
10
static NSString * const TWCDefaultChannelUniqueName = @"general";
11
static NSString * const TWCDefaultChannelName = @"General Channel";
12
13
static NSString * const TWCFriendlyNameKey = @"friendlyName";
14
15
@implementation ChannelManager
16
17
+ (instancetype)sharedManager {
18
static ChannelManager *sharedMyManager = nil;
19
static dispatch_once_t onceToken;
20
dispatch_once(&onceToken, ^{
21
sharedMyManager = [[self alloc] init];
22
});
23
return sharedMyManager;
24
}
25
26
- (instancetype)init {
27
self.channels = [[NSMutableOrderedSet alloc] init];
28
return self;
29
}
30
31
#pragma mark General channel
32
33
- (void)joinGeneralChatRoomWithCompletion:(SucceedHandler)completion {
34
[self.channelsList channelWithSidOrUniqueName:TWCDefaultChannelUniqueName completion:^(TCHResult *result, TCHChannel *channel) {
35
if ([result isSuccessful]) {
36
self.generalChannel = channel;
37
}
38
39
if (self.generalChannel) {
40
[self joinGeneralChatRoomWithUniqueName:nil completion:completion];
41
}
42
else {
43
[self createGeneralChatRoomWithCompletion:^(BOOL succeeded) {
44
if (succeeded) {
45
[self joinGeneralChatRoomWithUniqueName:TWCDefaultChannelUniqueName completion:completion];
46
return;
47
}
48
if (completion) completion(NO);
49
}];
50
};
51
}];
52
}
53
54
- (void)joinGeneralChatRoomWithUniqueName:(NSString *)uniqueName completion:(SucceedHandler)completion {
55
[self.generalChannel joinWithCompletion:^(TCHResult *result) {
56
if ([result isSuccessful]) {
57
if (uniqueName) {
58
[self setGeneralChatRoomUniqueNameWithCompletion:completion];
59
return;
60
}
61
}
62
if (completion) completion([result isSuccessful]);
63
}];
64
}
65
66
- (void)createGeneralChatRoomWithCompletion:(SucceedHandler)completion {
67
NSDictionary *options = [
68
NSDictionary
69
dictionaryWithObjectsAndKeys:TWCDefaultChannelName,
70
TCHChannelOptionFriendlyName,
71
TCHChannelTypePublic,
72
TCHChannelOptionType,
73
nil
74
];
75
76
[self.channelsList createChannelWithOptions:options
77
completion:^(TCHResult *result, TCHChannel *channel) {
78
if ([result isSuccessful]) {
79
self.generalChannel = channel;
80
}
81
if (completion) completion([result isSuccessful]);
82
}];
83
}
84
85
- (void)setGeneralChatRoomUniqueNameWithCompletion:(SucceedHandler)completion {
86
[self.generalChannel setUniqueName:TWCDefaultChannelUniqueName
87
completion:^(TCHResult *result) {
88
if (completion) completion([result isSuccessful]);
89
}];
90
}
91
92
#pragma mark Populate channels
93
94
- (void)populateChannels {
95
self.channels = [[NSMutableOrderedSet alloc] init];
96
[self.channelsList userChannelDescriptorsWithCompletion:^(TCHResult * _Nonnull result, TCHChannelDescriptorPaginator * _Nullable channelPaginator) {
97
[self.channels addObjectsFromArray:[channelPaginator items]];
98
[self sortAndDedupeChannels];
99
if (self.delegate) {
100
[self.delegate reloadChannelList];
101
}
102
}];
103
104
[self.channelsList publicChannelDescriptorsWithCompletion:^(TCHResult *result,
105
TCHChannelDescriptorPaginator *channelDescPaginator) {
106
[self.channels addObjectsFromArray: [channelDescPaginator items]];
107
[self sortAndDedupeChannels];
108
if (self.delegate) {
109
[self.delegate reloadChannelList];
110
}
111
}];
112
}
113
114
- (void)sortAndDedupeChannels {
115
NSMutableDictionary *channelsDict = [[NSMutableDictionary alloc] init];
116
117
for(TCHChannel *channel in self.channels) {
118
if (![channelsDict objectForKey: channel.sid] ||
119
![[channelsDict objectForKey: channel.sid] isKindOfClass: [NSNull class]]) {
120
[channelsDict setObject:channel forKey:channel.sid];
121
}
122
}
123
124
NSMutableOrderedSet *dedupedChannels = [NSMutableOrderedSet
125
orderedSetWithArray:[channelsDict allValues]];
126
127
SEL sortSelector = @selector(localizedCaseInsensitiveCompare:);
128
129
NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:TWCFriendlyNameKey
130
ascending:YES
131
selector:sortSelector];
132
133
[dedupedChannels sortUsingDescriptors:@[descriptor]];
134
135
self.channels = dedupedChannels;
136
}
137
138
# pragma mark Create channel
139
140
- (void)createChannelWithName:(NSString *)name completion:(ChannelHandler)completion {
141
if ([name isEqualToString:TWCDefaultChannelName]) {
142
if (completion) completion(NO, nil);
143
return;
144
}
145
146
NSDictionary *options = [
147
NSDictionary
148
dictionaryWithObjectsAndKeys:name,
149
TCHChannelOptionFriendlyName,
150
TCHChannelTypePublic,
151
TCHChannelOptionType,
152
nil
153
];
154
[self.channelsList
155
createChannelWithOptions:options
156
completion:^(TCHResult *result, TCHChannel *channel) {
157
[self.channels addObject:channel];
158
[self sortAndDedupeChannels];
159
if (completion) completion([result isSuccessful], channel);
160
}];
161
}
162
163
# pragma mark TwilioChatClientDelegate
164
165
- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel{
166
dispatch_async(dispatch_get_main_queue(), ^{
167
[self.channels addObject:channel];
168
[self sortAndDedupeChannels];
169
[self.delegate chatClient:client channelAdded:channel];
170
});
171
}
172
173
- (void)chatClient:(TwilioChatClient *)client channel:(nonnull TCHChannel *)channel updated:(TCHChannelUpdate)updated {
174
dispatch_async(dispatch_get_main_queue(), ^{
175
[self.delegate chatClient:client channel:channel updated:updated];
176
});
177
}
178
179
- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {
180
dispatch_async(dispatch_get_main_queue(), ^{
181
[[ChannelManager sharedManager].channels removeObject:channel];
182
[self.delegate chatClient:client channelDeleted:channel];
183
});
184
}
185
186
- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {
187
188
}
189
190
@end
191

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 destroyWithCompletion.