As the Programmable Chat API is set to sunset in 2022, we will no longer maintain these chat tutorials.
Please see our Conversations API QuickStart to start building robust virtual spaces for conversation.
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.
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:
For your convenience, we consolidated the source code for this tutorial in a single GitHub repository. Feel free to clone it and tweak it as required.
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 to make a request to our server and get the access token.
twiliochat/MessagingManager.m
1#import "MessagingManager.h"2#import "ChannelManager.h"3#import "SessionManager.h"4#import "TokenRequestHandler.h"56@interface MessagingManager ()7@property (strong, nonatomic) TwilioChatClient *client;8@property (nonatomic, getter=isConnected) BOOL connected;9@end1011static NSString * const TWCLoginViewControllerName = @"LoginViewController";12static NSString * const TWCMainViewControllerName = @"RevealViewController";1314static NSString * const TWCTokenKey = @"token";1516@implementation MessagingManager17+ (instancetype)sharedManager {18static MessagingManager *sharedMyManager = nil;19static dispatch_once_t onceToken;20dispatch_once(&onceToken, ^{21sharedMyManager = [[self alloc] init];22});23return sharedMyManager;24}2526- (instancetype)init {27self.delegate = [ChannelManager sharedManager];28return self;29}3031# pragma mark Present view controllers3233- (void)presentRootViewController {34if (!self.isLoggedIn) {35[self presentViewControllerByName:TWCLoginViewControllerName];36return;37}38if (!self.isConnected) {39[self connectClientWithCompletion:^(BOOL success, NSError *error) {40if (success) {41NSLog(@"Successfully connected chat client");42}43}];44}4546}4748- (void)presentViewControllerByName:(NSString *)viewController {49[self presentViewController:[[self storyboardWithName:@"Main"] instantiateViewControllerWithIdentifier:viewController]];50}5152- (void)presentLaunchScreen {53[self presentViewController:[[self storyboardWithName:@"LaunchScreen"] instantiateInitialViewController]];54}5556- (void)presentViewController:(UIViewController *)viewController {57UIWindow *window = [[UIApplication sharedApplication].delegate window];58window.rootViewController = viewController;59}6061- (UIStoryboard *)storyboardWithName:(NSString *)name {62return [UIStoryboard storyboardWithName: name bundle: [NSBundle mainBundle]];63}6465# pragma mark User and session management6667- (BOOL)isLoggedIn {68return [SessionManager isLoggedIn];69}7071- (void)loginWithUsername:(NSString *)username72completion:(StatusWithErrorHandler)completion {73[SessionManager loginWithUsername:username];74[self connectClientWithCompletion:^(BOOL success, NSError *error) {75if (success) {76[self presentViewControllerByName:TWCMainViewControllerName];77}78completion(success, error);79}];80}8182- (void)logout {83[SessionManager logout];84self.connected = NO;8586dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{87[self.client shutdown];88self.client = nil;89});90}9192# pragma mark Twilio client9394- (void)connectClientWithCompletion:(StatusWithErrorHandler)completion {95if (self.client) {96[self logout];97}9899[self requestTokenWithCompletion:^(BOOL succeeded, NSString *token) {100if (succeeded) {101[self initializeClientWithToken:token];102if (completion) completion(succeeded, nil);103}104else {105NSError *error = [self errorWithDescription:@"Could not get access token" code:301];106if (completion) completion(succeeded, error);107}108}];109}110111- (void)initializeClientWithToken:(NSString *)token {112[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;113114[TwilioChatClient chatClientWithToken:token115properties:nil116delegate:self117completion:^(TCHResult * _Nonnull result, TwilioChatClient * _Nullable chatClient) {118if (result.isSuccessful) {119self.client = chatClient;120self.connected = YES;121122}123}];124}125126- (void)requestTokenWithCompletion:(StatusWithTokenHandler)completion {127NSString *uuid = [[UIDevice currentDevice] identifierForVendor].UUIDString;128NSDictionary *parameters = @{@"device": uuid, @"identity": [SessionManager getUsername]};129130[TokenRequestHandler fetchTokenWithParams:parameters completion:^(NSDictionary *results, NSError *error) {131NSString *token = [results objectForKey:TWCTokenKey];132BOOL errorCondition = error || !token;133134if (completion) completion(!errorCondition, token);135}];136}137138- (void)loadGeneralChatRoomWithCompletion:(StatusWithErrorHandler)completion {139[[ChannelManager sharedManager] joinGeneralChatRoomWithCompletion:^(BOOL succeeded) {140if (succeeded)141{142if (completion) completion(succeeded, nil);143}144else {145NSError *error = [self errorWithDescription:@"Could not join General channel" code:300];146if (completion) completion(succeeded, error);147}148}];149}150151- (NSError *)errorWithDescription:(NSString *)description code:(NSInteger)code {152NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};153NSError *error = [NSError errorWithDomain:@"app" code:code userInfo:userInfo];154return error;155}156157#pragma mark Internal helpers158159- (NSString *)userIdentity {160return [SessionManager getUsername];161}162163- (void)refreshChatToken:(TwilioChatClient*)client {164[self requestTokenWithCompletion:^(BOOL succeeded, NSString *token) {165if (succeeded) {166[client updateToken:token completion:^(TCHResult * _Nonnull result) {167if (result.isSuccessful) {168169}170}];171}172else {173NSLog(@"Error while trying to get new access token");174}175}];176}177178#pragma mark TwilioChatClientDelegate179180- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel {181[self.delegate chatClient:client channelAdded:channel];182}183184- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {185[self.delegate chatClient:client channelDeleted:channel];186}187188- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {189if (status == TCHClientSynchronizationStatusCompleted) {190[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;191[ChannelManager sharedManager].channelsList = client.channelsList;192[[ChannelManager sharedManager] populateChannels];193[self loadGeneralChatRoomWithCompletion:^(BOOL success, NSError *error) {194if (success) {195[self presentViewControllerByName:TWCMainViewControllerName];196}197}];198}199[self.delegate chatClient:client synchronizationStatusUpdated:status];200}201202- (void)chatClientTokenWillExpire:(TwilioChatClient *)client {203[self refreshChatToken:client];204}205206- (void)chatClientTokenExpired:(TwilioChatClient *)client {207[self refreshChatToken:client];208}209210@end211
Now it's time to synchronize your Twilio client.
The synchronizationStatusUpdated
delegate 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 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() will return nil
.
twiliochat/MessagingManager.m
1#import "MessagingManager.h"2#import "ChannelManager.h"3#import "SessionManager.h"4#import "TokenRequestHandler.h"56@interface MessagingManager ()7@property (strong, nonatomic) TwilioChatClient *client;8@property (nonatomic, getter=isConnected) BOOL connected;9@end1011static NSString * const TWCLoginViewControllerName = @"LoginViewController";12static NSString * const TWCMainViewControllerName = @"RevealViewController";1314static NSString * const TWCTokenKey = @"token";1516@implementation MessagingManager17+ (instancetype)sharedManager {18static MessagingManager *sharedMyManager = nil;19static dispatch_once_t onceToken;20dispatch_once(&onceToken, ^{21sharedMyManager = [[self alloc] init];22});23return sharedMyManager;24}2526- (instancetype)init {27self.delegate = [ChannelManager sharedManager];28return self;29}3031# pragma mark Present view controllers3233- (void)presentRootViewController {34if (!self.isLoggedIn) {35[self presentViewControllerByName:TWCLoginViewControllerName];36return;37}38if (!self.isConnected) {39[self connectClientWithCompletion:^(BOOL success, NSError *error) {40if (success) {41NSLog(@"Successfully connected chat client");42}43}];44}4546}4748- (void)presentViewControllerByName:(NSString *)viewController {49[self presentViewController:[[self storyboardWithName:@"Main"] instantiateViewControllerWithIdentifier:viewController]];50}5152- (void)presentLaunchScreen {53[self presentViewController:[[self storyboardWithName:@"LaunchScreen"] instantiateInitialViewController]];54}5556- (void)presentViewController:(UIViewController *)viewController {57UIWindow *window = [[UIApplication sharedApplication].delegate window];58window.rootViewController = viewController;59}6061- (UIStoryboard *)storyboardWithName:(NSString *)name {62return [UIStoryboard storyboardWithName: name bundle: [NSBundle mainBundle]];63}6465# pragma mark User and session management6667- (BOOL)isLoggedIn {68return [SessionManager isLoggedIn];69}7071- (void)loginWithUsername:(NSString *)username72completion:(StatusWithErrorHandler)completion {73[SessionManager loginWithUsername:username];74[self connectClientWithCompletion:^(BOOL success, NSError *error) {75if (success) {76[self presentViewControllerByName:TWCMainViewControllerName];77}78completion(success, error);79}];80}8182- (void)logout {83[SessionManager logout];84self.connected = NO;8586dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{87[self.client shutdown];88self.client = nil;89});90}9192# pragma mark Twilio client9394- (void)connectClientWithCompletion:(StatusWithErrorHandler)completion {95if (self.client) {96[self logout];97}9899[self requestTokenWithCompletion:^(BOOL succeeded, NSString *token) {100if (succeeded) {101[self initializeClientWithToken:token];102if (completion) completion(succeeded, nil);103}104else {105NSError *error = [self errorWithDescription:@"Could not get access token" code:301];106if (completion) completion(succeeded, error);107}108}];109}110111- (void)initializeClientWithToken:(NSString *)token {112[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;113114[TwilioChatClient chatClientWithToken:token115properties:nil116delegate:self117completion:^(TCHResult * _Nonnull result, TwilioChatClient * _Nullable chatClient) {118if (result.isSuccessful) {119self.client = chatClient;120self.connected = YES;121122}123}];124}125126- (void)requestTokenWithCompletion:(StatusWithTokenHandler)completion {127NSString *uuid = [[UIDevice currentDevice] identifierForVendor].UUIDString;128NSDictionary *parameters = @{@"device": uuid, @"identity": [SessionManager getUsername]};129130[TokenRequestHandler fetchTokenWithParams:parameters completion:^(NSDictionary *results, NSError *error) {131NSString *token = [results objectForKey:TWCTokenKey];132BOOL errorCondition = error || !token;133134if (completion) completion(!errorCondition, token);135}];136}137138- (void)loadGeneralChatRoomWithCompletion:(StatusWithErrorHandler)completion {139[[ChannelManager sharedManager] joinGeneralChatRoomWithCompletion:^(BOOL succeeded) {140if (succeeded)141{142if (completion) completion(succeeded, nil);143}144else {145NSError *error = [self errorWithDescription:@"Could not join General channel" code:300];146if (completion) completion(succeeded, error);147}148}];149}150151- (NSError *)errorWithDescription:(NSString *)description code:(NSInteger)code {152NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};153NSError *error = [NSError errorWithDomain:@"app" code:code userInfo:userInfo];154return error;155}156157#pragma mark Internal helpers158159- (NSString *)userIdentity {160return [SessionManager getUsername];161}162163- (void)refreshChatToken:(TwilioChatClient*)client {164[self requestTokenWithCompletion:^(BOOL succeeded, NSString *token) {165if (succeeded) {166[client updateToken:token completion:^(TCHResult * _Nonnull result) {167if (result.isSuccessful) {168169}170}];171}172else {173NSLog(@"Error while trying to get new access token");174}175}];176}177178#pragma mark TwilioChatClientDelegate179180- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel {181[self.delegate chatClient:client channelAdded:channel];182}183184- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {185[self.delegate chatClient:client channelDeleted:channel];186}187188- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {189if (status == TCHClientSynchronizationStatusCompleted) {190[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;191[ChannelManager sharedManager].channelsList = client.channelsList;192[[ChannelManager sharedManager] populateChannels];193[self loadGeneralChatRoomWithCompletion:^(BOOL success, NSError *error) {194if (success) {195[self presentViewControllerByName:TWCMainViewControllerName];196}197}];198}199[self.delegate chatClient:client synchronizationStatusUpdated:status];200}201202- (void)chatClientTokenWillExpire:(TwilioChatClient *)client {203[self refreshChatToken:client];204}205206- (void)chatClientTokenExpired:(TwilioChatClient *)client {207[self refreshChatToken:client];208}209210@end211
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 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"34#define _ Underscore56@interface ChannelManager ()7@property (strong, nonatomic) TCHChannel *generalChannel;8@end910static NSString * const TWCDefaultChannelUniqueName = @"general";11static NSString * const TWCDefaultChannelName = @"General Channel";1213static NSString * const TWCFriendlyNameKey = @"friendlyName";1415@implementation ChannelManager1617+ (instancetype)sharedManager {18static ChannelManager *sharedMyManager = nil;19static dispatch_once_t onceToken;20dispatch_once(&onceToken, ^{21sharedMyManager = [[self alloc] init];22});23return sharedMyManager;24}2526- (instancetype)init {27self.channels = [[NSMutableOrderedSet alloc] init];28return self;29}3031#pragma mark General channel3233- (void)joinGeneralChatRoomWithCompletion:(SucceedHandler)completion {34[self.channelsList channelWithSidOrUniqueName:TWCDefaultChannelUniqueName completion:^(TCHResult *result, TCHChannel *channel) {35if ([result isSuccessful]) {36self.generalChannel = channel;37}3839if (self.generalChannel) {40[self joinGeneralChatRoomWithUniqueName:nil completion:completion];41}42else {43[self createGeneralChatRoomWithCompletion:^(BOOL succeeded) {44if (succeeded) {45[self joinGeneralChatRoomWithUniqueName:TWCDefaultChannelUniqueName completion:completion];46return;47}48if (completion) completion(NO);49}];50};51}];52}5354- (void)joinGeneralChatRoomWithUniqueName:(NSString *)uniqueName completion:(SucceedHandler)completion {55[self.generalChannel joinWithCompletion:^(TCHResult *result) {56if ([result isSuccessful]) {57if (uniqueName) {58[self setGeneralChatRoomUniqueNameWithCompletion:completion];59return;60}61}62if (completion) completion([result isSuccessful]);63}];64}6566- (void)createGeneralChatRoomWithCompletion:(SucceedHandler)completion {67NSDictionary *options = [68NSDictionary69dictionaryWithObjectsAndKeys:TWCDefaultChannelName,70TCHChannelOptionFriendlyName,71TCHChannelTypePublic,72TCHChannelOptionType,73nil74];7576[self.channelsList createChannelWithOptions:options77completion:^(TCHResult *result, TCHChannel *channel) {78if ([result isSuccessful]) {79self.generalChannel = channel;80}81if (completion) completion([result isSuccessful]);82}];83}8485- (void)setGeneralChatRoomUniqueNameWithCompletion:(SucceedHandler)completion {86[self.generalChannel setUniqueName:TWCDefaultChannelUniqueName87completion:^(TCHResult *result) {88if (completion) completion([result isSuccessful]);89}];90}9192#pragma mark Populate channels9394- (void)populateChannels {95self.channels = [[NSMutableOrderedSet alloc] init];96[self.channelsList userChannelDescriptorsWithCompletion:^(TCHResult * _Nonnull result, TCHChannelDescriptorPaginator * _Nullable channelPaginator) {97[self.channels addObjectsFromArray:[channelPaginator items]];98[self sortAndDedupeChannels];99if (self.delegate) {100[self.delegate reloadChannelList];101}102}];103104[self.channelsList publicChannelDescriptorsWithCompletion:^(TCHResult *result,105TCHChannelDescriptorPaginator *channelDescPaginator) {106[self.channels addObjectsFromArray: [channelDescPaginator items]];107[self sortAndDedupeChannels];108if (self.delegate) {109[self.delegate reloadChannelList];110}111}];112}113114- (void)sortAndDedupeChannels {115NSMutableDictionary *channelsDict = [[NSMutableDictionary alloc] init];116117for(TCHChannel *channel in self.channels) {118if (![channelsDict objectForKey: channel.sid] ||119![[channelsDict objectForKey: channel.sid] isKindOfClass: [NSNull class]]) {120[channelsDict setObject:channel forKey:channel.sid];121}122}123124NSMutableOrderedSet *dedupedChannels = [NSMutableOrderedSet125orderedSetWithArray:[channelsDict allValues]];126127SEL sortSelector = @selector(localizedCaseInsensitiveCompare:);128129NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:TWCFriendlyNameKey130ascending:YES131selector:sortSelector];132133[dedupedChannels sortUsingDescriptors:@[descriptor]];134135self.channels = dedupedChannels;136}137138# pragma mark Create channel139140- (void)createChannelWithName:(NSString *)name completion:(ChannelHandler)completion {141if ([name isEqualToString:TWCDefaultChannelName]) {142if (completion) completion(NO, nil);143return;144}145146NSDictionary *options = [147NSDictionary148dictionaryWithObjectsAndKeys:name,149TCHChannelOptionFriendlyName,150TCHChannelTypePublic,151TCHChannelOptionType,152nil153];154[self.channelsList155createChannelWithOptions:options156completion:^(TCHResult *result, TCHChannel *channel) {157[self.channels addObject:channel];158[self sortAndDedupeChannels];159if (completion) completion([result isSuccessful], channel);160}];161}162163# pragma mark TwilioChatClientDelegate164165- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel{166dispatch_async(dispatch_get_main_queue(), ^{167[self.channels addObject:channel];168[self sortAndDedupeChannels];169[self.delegate chatClient:client channelAdded:channel];170});171}172173- (void)chatClient:(TwilioChatClient *)client channel:(nonnull TCHChannel *)channel updated:(TCHChannelUpdate)updated {174dispatch_async(dispatch_get_main_queue(), ^{175[self.delegate chatClient:client channel:channel updated:updated];176});177}178179- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {180dispatch_async(dispatch_get_main_queue(), ^{181[[ChannelManager sharedManager].channels removeObject:channel];182[self.delegate chatClient:client channelDeleted:channel];183});184}185186- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {187188}189190@end191
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
. 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.
twiliochat/ChannelManager.m
1#import "ChannelManager.h"2#import "MessagingManager.h"34#define _ Underscore56@interface ChannelManager ()7@property (strong, nonatomic) TCHChannel *generalChannel;8@end910static NSString * const TWCDefaultChannelUniqueName = @"general";11static NSString * const TWCDefaultChannelName = @"General Channel";1213static NSString * const TWCFriendlyNameKey = @"friendlyName";1415@implementation ChannelManager1617+ (instancetype)sharedManager {18static ChannelManager *sharedMyManager = nil;19static dispatch_once_t onceToken;20dispatch_once(&onceToken, ^{21sharedMyManager = [[self alloc] init];22});23return sharedMyManager;24}2526- (instancetype)init {27self.channels = [[NSMutableOrderedSet alloc] init];28return self;29}3031#pragma mark General channel3233- (void)joinGeneralChatRoomWithCompletion:(SucceedHandler)completion {34[self.channelsList channelWithSidOrUniqueName:TWCDefaultChannelUniqueName completion:^(TCHResult *result, TCHChannel *channel) {35if ([result isSuccessful]) {36self.generalChannel = channel;37}3839if (self.generalChannel) {40[self joinGeneralChatRoomWithUniqueName:nil completion:completion];41}42else {43[self createGeneralChatRoomWithCompletion:^(BOOL succeeded) {44if (succeeded) {45[self joinGeneralChatRoomWithUniqueName:TWCDefaultChannelUniqueName completion:completion];46return;47}48if (completion) completion(NO);49}];50};51}];52}5354- (void)joinGeneralChatRoomWithUniqueName:(NSString *)uniqueName completion:(SucceedHandler)completion {55[self.generalChannel joinWithCompletion:^(TCHResult *result) {56if ([result isSuccessful]) {57if (uniqueName) {58[self setGeneralChatRoomUniqueNameWithCompletion:completion];59return;60}61}62if (completion) completion([result isSuccessful]);63}];64}6566- (void)createGeneralChatRoomWithCompletion:(SucceedHandler)completion {67NSDictionary *options = [68NSDictionary69dictionaryWithObjectsAndKeys:TWCDefaultChannelName,70TCHChannelOptionFriendlyName,71TCHChannelTypePublic,72TCHChannelOptionType,73nil74];7576[self.channelsList createChannelWithOptions:options77completion:^(TCHResult *result, TCHChannel *channel) {78if ([result isSuccessful]) {79self.generalChannel = channel;80}81if (completion) completion([result isSuccessful]);82}];83}8485- (void)setGeneralChatRoomUniqueNameWithCompletion:(SucceedHandler)completion {86[self.generalChannel setUniqueName:TWCDefaultChannelUniqueName87completion:^(TCHResult *result) {88if (completion) completion([result isSuccessful]);89}];90}9192#pragma mark Populate channels9394- (void)populateChannels {95self.channels = [[NSMutableOrderedSet alloc] init];96[self.channelsList userChannelDescriptorsWithCompletion:^(TCHResult * _Nonnull result, TCHChannelDescriptorPaginator * _Nullable channelPaginator) {97[self.channels addObjectsFromArray:[channelPaginator items]];98[self sortAndDedupeChannels];99if (self.delegate) {100[self.delegate reloadChannelList];101}102}];103104[self.channelsList publicChannelDescriptorsWithCompletion:^(TCHResult *result,105TCHChannelDescriptorPaginator *channelDescPaginator) {106[self.channels addObjectsFromArray: [channelDescPaginator items]];107[self sortAndDedupeChannels];108if (self.delegate) {109[self.delegate reloadChannelList];110}111}];112}113114- (void)sortAndDedupeChannels {115NSMutableDictionary *channelsDict = [[NSMutableDictionary alloc] init];116117for(TCHChannel *channel in self.channels) {118if (![channelsDict objectForKey: channel.sid] ||119![[channelsDict objectForKey: channel.sid] isKindOfClass: [NSNull class]]) {120[channelsDict setObject:channel forKey:channel.sid];121}122}123124NSMutableOrderedSet *dedupedChannels = [NSMutableOrderedSet125orderedSetWithArray:[channelsDict allValues]];126127SEL sortSelector = @selector(localizedCaseInsensitiveCompare:);128129NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:TWCFriendlyNameKey130ascending:YES131selector:sortSelector];132133[dedupedChannels sortUsingDescriptors:@[descriptor]];134135self.channels = dedupedChannels;136}137138# pragma mark Create channel139140- (void)createChannelWithName:(NSString *)name completion:(ChannelHandler)completion {141if ([name isEqualToString:TWCDefaultChannelName]) {142if (completion) completion(NO, nil);143return;144}145146NSDictionary *options = [147NSDictionary148dictionaryWithObjectsAndKeys:name,149TCHChannelOptionFriendlyName,150TCHChannelTypePublic,151TCHChannelOptionType,152nil153];154[self.channelsList155createChannelWithOptions:options156completion:^(TCHResult *result, TCHChannel *channel) {157[self.channels addObject:channel];158[self sortAndDedupeChannels];159if (completion) completion([result isSuccessful], channel);160}];161}162163# pragma mark TwilioChatClientDelegate164165- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel{166dispatch_async(dispatch_get_main_queue(), ^{167[self.channels addObject:channel];168[self sortAndDedupeChannels];169[self.delegate chatClient:client channelAdded:channel];170});171}172173- (void)chatClient:(TwilioChatClient *)client channel:(nonnull TCHChannel *)channel updated:(TCHChannelUpdate)updated {174dispatch_async(dispatch_get_main_queue(), ^{175[self.delegate chatClient:client channel:channel updated:updated];176});177}178179- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {180dispatch_async(dispatch_get_main_queue(), ^{181[[ChannelManager sharedManager].channels removeObject:channel];182[self.delegate chatClient:client channelDeleted:channel];183});184}185186- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {187188}189190@end191
Next, we need a default channel.
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.
twiliochat/ChannelManager.m
1#import "ChannelManager.h"2#import "MessagingManager.h"34#define _ Underscore56@interface ChannelManager ()7@property (strong, nonatomic) TCHChannel *generalChannel;8@end910static NSString * const TWCDefaultChannelUniqueName = @"general";11static NSString * const TWCDefaultChannelName = @"General Channel";1213static NSString * const TWCFriendlyNameKey = @"friendlyName";1415@implementation ChannelManager1617+ (instancetype)sharedManager {18static ChannelManager *sharedMyManager = nil;19static dispatch_once_t onceToken;20dispatch_once(&onceToken, ^{21sharedMyManager = [[self alloc] init];22});23return sharedMyManager;24}2526- (instancetype)init {27self.channels = [[NSMutableOrderedSet alloc] init];28return self;29}3031#pragma mark General channel3233- (void)joinGeneralChatRoomWithCompletion:(SucceedHandler)completion {34[self.channelsList channelWithSidOrUniqueName:TWCDefaultChannelUniqueName completion:^(TCHResult *result, TCHChannel *channel) {35if ([result isSuccessful]) {36self.generalChannel = channel;37}3839if (self.generalChannel) {40[self joinGeneralChatRoomWithUniqueName:nil completion:completion];41}42else {43[self createGeneralChatRoomWithCompletion:^(BOOL succeeded) {44if (succeeded) {45[self joinGeneralChatRoomWithUniqueName:TWCDefaultChannelUniqueName completion:completion];46return;47}48if (completion) completion(NO);49}];50};51}];52}5354- (void)joinGeneralChatRoomWithUniqueName:(NSString *)uniqueName completion:(SucceedHandler)completion {55[self.generalChannel joinWithCompletion:^(TCHResult *result) {56if ([result isSuccessful]) {57if (uniqueName) {58[self setGeneralChatRoomUniqueNameWithCompletion:completion];59return;60}61}62if (completion) completion([result isSuccessful]);63}];64}6566- (void)createGeneralChatRoomWithCompletion:(SucceedHandler)completion {67NSDictionary *options = [68NSDictionary69dictionaryWithObjectsAndKeys:TWCDefaultChannelName,70TCHChannelOptionFriendlyName,71TCHChannelTypePublic,72TCHChannelOptionType,73nil74];7576[self.channelsList createChannelWithOptions:options77completion:^(TCHResult *result, TCHChannel *channel) {78if ([result isSuccessful]) {79self.generalChannel = channel;80}81if (completion) completion([result isSuccessful]);82}];83}8485- (void)setGeneralChatRoomUniqueNameWithCompletion:(SucceedHandler)completion {86[self.generalChannel setUniqueName:TWCDefaultChannelUniqueName87completion:^(TCHResult *result) {88if (completion) completion([result isSuccessful]);89}];90}9192#pragma mark Populate channels9394- (void)populateChannels {95self.channels = [[NSMutableOrderedSet alloc] init];96[self.channelsList userChannelDescriptorsWithCompletion:^(TCHResult * _Nonnull result, TCHChannelDescriptorPaginator * _Nullable channelPaginator) {97[self.channels addObjectsFromArray:[channelPaginator items]];98[self sortAndDedupeChannels];99if (self.delegate) {100[self.delegate reloadChannelList];101}102}];103104[self.channelsList publicChannelDescriptorsWithCompletion:^(TCHResult *result,105TCHChannelDescriptorPaginator *channelDescPaginator) {106[self.channels addObjectsFromArray: [channelDescPaginator items]];107[self sortAndDedupeChannels];108if (self.delegate) {109[self.delegate reloadChannelList];110}111}];112}113114- (void)sortAndDedupeChannels {115NSMutableDictionary *channelsDict = [[NSMutableDictionary alloc] init];116117for(TCHChannel *channel in self.channels) {118if (![channelsDict objectForKey: channel.sid] ||119![[channelsDict objectForKey: channel.sid] isKindOfClass: [NSNull class]]) {120[channelsDict setObject:channel forKey:channel.sid];121}122}123124NSMutableOrderedSet *dedupedChannels = [NSMutableOrderedSet125orderedSetWithArray:[channelsDict allValues]];126127SEL sortSelector = @selector(localizedCaseInsensitiveCompare:);128129NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:TWCFriendlyNameKey130ascending:YES131selector:sortSelector];132133[dedupedChannels sortUsingDescriptors:@[descriptor]];134135self.channels = dedupedChannels;136}137138# pragma mark Create channel139140- (void)createChannelWithName:(NSString *)name completion:(ChannelHandler)completion {141if ([name isEqualToString:TWCDefaultChannelName]) {142if (completion) completion(NO, nil);143return;144}145146NSDictionary *options = [147NSDictionary148dictionaryWithObjectsAndKeys:name,149TCHChannelOptionFriendlyName,150TCHChannelTypePublic,151TCHChannelOptionType,152nil153];154[self.channelsList155createChannelWithOptions:options156completion:^(TCHResult *result, TCHChannel *channel) {157[self.channels addObject:channel];158[self sortAndDedupeChannels];159if (completion) completion([result isSuccessful], channel);160}];161}162163# pragma mark TwilioChatClientDelegate164165- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel{166dispatch_async(dispatch_get_main_queue(), ^{167[self.channels addObject:channel];168[self sortAndDedupeChannels];169[self.delegate chatClient:client channelAdded:channel];170});171}172173- (void)chatClient:(TwilioChatClient *)client channel:(nonnull TCHChannel *)channel updated:(TCHChannelUpdate)updated {174dispatch_async(dispatch_get_main_queue(), ^{175[self.delegate chatClient:client channel:channel updated:updated];176});177}178179- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {180dispatch_async(dispatch_get_main_queue(), ^{181[[ChannelManager sharedManager].channels removeObject:channel];182[self.delegate chatClient:client channelDeleted:channel];183});184}185186- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {187188}189190@end191
Now let's listen for some channel events.
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"1011@interface MainChatViewController ()12@property (weak, nonatomic) IBOutlet UIBarButtonItem *revealButtonItem;13@property (weak, nonatomic) IBOutlet UIBarButtonItem *actionButtonItem;1415@property (strong, nonatomic) NSMutableOrderedSet *messages;1617@end1819static NSString * const TWCChatCellIdentifier = @"ChatTableCell";20static NSString * const TWCChatStatusCellIdentifier = @"ChatStatusTableCell";2122static NSString * const TWCOpenGeneralChannelSegue = @"OpenGeneralChat";23static NSInteger const TWCLabelTag = 200;2425@implementation MainChatViewController2627#pragma mark Initialization2829- (void)viewDidLoad {30[super viewDidLoad];3132if (self.revealViewController)33{34[self.revealButtonItem setTarget: self.revealViewController];35[self.revealButtonItem setAction: @selector( revealToggle: )];36[self.navigationController.navigationBar addGestureRecognizer: self.revealViewController.panGestureRecognizer];37self.revealViewController.rearViewRevealOverdraw = 0.f;38}3940self.bounces = YES;41self.shakeToClearEnabled = YES;42self.keyboardPanningEnabled = YES;43self.shouldScrollToBottomAfterKeyboardShows = NO;44self.inverted = YES;4546UINib *cellNib = [UINib nibWithNibName:TWCChatCellIdentifier bundle:nil];47[self.tableView registerNib:cellNib48forCellReuseIdentifier:TWCChatCellIdentifier];4950UINib *cellStatusNib = [UINib nibWithNibName:TWCChatStatusCellIdentifier bundle:nil];51[self.tableView registerNib:cellStatusNib52forCellReuseIdentifier:TWCChatStatusCellIdentifier];5354self.textInputbar.autoHideRightButton = YES;55self.textInputbar.maxCharCount = 256;56self.textInputbar.counterStyle = SLKCounterStyleSplit;57self.textInputbar.counterPosition = SLKCounterPositionTop;5859UIFont *font = [UIFont fontWithName:@"Avenir-Light" size:14];60self.textView.font = font;6162[self.rightButton setTitleColor:[UIColor colorWithRed:0.973 green:0.557 blue:0.502 alpha:1]63forState:UIControlStateNormal];6465font = [UIFont fontWithName:@"Avenir-Heavy" size:17];66self.navigationController.navigationBar.titleTextAttributes = @{NSFontAttributeName:font};6768self.tableView.allowsSelection = NO;69self.tableView.estimatedRowHeight = 70;70self.tableView.rowHeight = UITableViewAutomaticDimension;71self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;7273if (!self.channel) {74id generalChannel = [ChannelManager sharedManager].generalChannel;75if (generalChannel) {76self.channel = generalChannel;77} else {78[[ChannelManager sharedManager] joinGeneralChatRoomWithCompletion:^(BOOL succeeded) {79if (succeeded) {80self.channel = [ChannelManager sharedManager].generalChannel;81}82}];83}84}85}8687- (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}9394- (void)viewDidAppear:(BOOL)animated {95[super viewDidAppear:animated];96[self scrollToBottomMessage];97}9899- (NSMutableOrderedSet *)messages {100if (!_messages) {101_messages = [[NSMutableOrderedSet alloc] init];102}103return _messages;104}105106- (void)setChannel:(TCHChannel *)channel {107if ([channel isKindOfClass:[TCHChannelDescriptor class]]) {108TCHChannelDescriptor *channelDescriptor = (TCHChannelDescriptor*)channel;109[channelDescriptor channelWithCompletion:^(TCHResult *success, TCHChannel *channel) {110if (success) {111[self actuallySetChannel:channel];112}113}];114} else {115[self actuallySetChannel:channel];116}117}118119- (void)actuallySetChannel:(TCHChannel *)channel {120_channel = channel;121self.title = self.channel.friendlyName;122self.channel.delegate = self;123124if (self.channel == [ChannelManager sharedManager].generalChannel) {125self.navigationItem.rightBarButtonItem = nil;126}127128[self setViewOnHold:YES];129130if (self.channel.status != TCHChannelStatusJoined) {131[self.channel joinWithCompletion:^(TCHResult* result) {132NSLog(@"%@", @"Channel Joined");133[self setViewOnHold:NO];134}];135}136if (self.channel.synchronizationStatus == TCHChannelSynchronizationStatusAll) {137[self loadMessages];138[self setViewOnHold:NO];139}140}141142// Disable user input and show activity indicator143- (void)setViewOnHold:(BOOL)onHold {144self.textInputbarHidden = onHold;145[UIApplication sharedApplication].networkActivityIndicatorVisible = onHold;146}147148- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {149return 1;150}151152- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {153return self.messages.count;154}155156- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {157UITableViewCell *cell = nil;158159id message = [self.messages objectAtIndex:indexPath.row];160161if ([message isKindOfClass:[TCHMessage class]]) {162cell = [self getChatCellForTableView:tableView forIndexPath:indexPath message:message];163}164else {165cell = [self getStatusCellForTableView:tableView forIndexPath:indexPath message:message];166}167168cell.transform = tableView.transform;169return cell;170}171172- (ChatTableCell *)getChatCellForTableView:(UITableView *)tableView173forIndexPath:(NSIndexPath *)indexPath174message:(TCHMessage *)message {175UITableViewCell *cell = [self.tableView176dequeueReusableCellWithIdentifier:TWCChatCellIdentifier forIndexPath:indexPath];177178ChatTableCell *chatCell = (ChatTableCell *)cell;179chatCell.user = message.author;180chatCell.date = [[[DateTodayFormatter alloc] init]181stringFromDate:[NSDate dateWithISO8601String:message.timestamp]];182183chatCell.message = message.body;184185return chatCell;186}187188- (UITableViewCell *)getStatusCellForTableView:(UITableView *)tableView189forIndexPath:(NSIndexPath *)indexPath190message:(StatusEntry *)message {191UITableViewCell *cell = [self.tableView192dequeueReusableCellWithIdentifier:TWCChatStatusCellIdentifier forIndexPath:indexPath];193194UILabel *label = [cell viewWithTag:TWCLabelTag];195label.text = [NSString stringWithFormat:@"User %@ has %@",196message.member.identity, (message.status == TWCMemberStatusJoined) ? @"joined" : @"left"];197198return cell;199}200201- (void)didPressRightButton:(id)sender {202[self.textView refreshFirstResponder];203[self sendMessage: [self.textView.text copy]];204[super didPressRightButton:sender];205}206207#pragma mark Chat Service208- (void)sendMessage: (NSString *)inputMessage {209TCHMessageOptions *messageOptions = [[[TCHMessageOptions alloc] init] withBody:inputMessage];210[self.channel.messages sendMessageWithOptions:messageOptions211completion:nil];212}213214215216- (void)addMessages:(NSArray *)messages {217[self.messages addObjectsFromArray:messages];218[self sortMessages];219dispatch_async(dispatch_get_main_queue(), ^{220[self.tableView reloadData];221if (self.messages.count > 0) {222[self scrollToBottomMessage];223}224});225}226227228- (void)sortMessages {229[self.messages sortUsingDescriptors:@[[[NSSortDescriptor alloc]230initWithKey:@"timestamp" ascending:NO]]];231}232233- (void)scrollToBottomMessage {234if (self.messages.count == 0) {235return;236}237238NSIndexPath *bottomMessageIndex = [NSIndexPath indexPathForRow:0239inSection:0];240[self.tableView scrollToRowAtIndexPath:bottomMessageIndex241atScrollPosition:UITableViewScrollPositionBottom animated:NO];242}243244- (void)loadMessages {245[self.messages removeAllObjects];246if (self.channel.synchronizationStatus == TCHChannelSynchronizationStatusAll) {247[self.channel.messages248getLastMessagesWithCount:100249completion:^(TCHResult *result, NSArray *messages) {250if ([result isSuccessful]) {251[self addMessages: messages];252}253}];254}255}256257- (void)leaveChannel {258[self.channel leaveWithCompletion:^(TCHResult* result) {259if ([result isSuccessful]) {260[(MenuViewController *)self.revealViewController.rearViewController deselectSelectedChannel];261[self.revealViewController.rearViewController262performSegueWithIdentifier:TWCOpenGeneralChannelSegue sender:nil];263}264}];265}266267#pragma mark - TMMessageDelegate268269- (void)chatClient:(TwilioChatClient *)client270channel:(TCHChannel *)channel271messageAdded:(TCHMessage *)message {272if (![self.messages containsObject:message]) {273[self addMessages:@[message]];274}275}276277- (void)chatClient:(TwilioChatClient *)client278channelDeleted:(TCHChannel *)channel {279dispatch_async(dispatch_get_main_queue(), ^{280if (channel == self.channel) {281[self.revealViewController.rearViewController282performSegueWithIdentifier:TWCOpenGeneralChannelSegue sender:nil];283}284});285}286287- (void)chatClient:(TwilioChatClient *)client288channel:(TCHChannel *)channel289memberJoined:(TCHMember *)member {290[self addMessages:@[[StatusEntry statusEntryWithMember:member status:TWCMemberStatusJoined]]];291}292293- (void)chatClient:(TwilioChatClient *)client294channel:(TCHChannel *)channel295memberLeft:(TCHMember *)member {296[self addMessages:@[[StatusEntry statusEntryWithMember:member status:TWCMemberStatusLeft]]];297}298299- (void)chatClient:(TwilioChatClient *)client channel:(TCHChannel *)channel synchronizationStatusChanged:(TCHChannelSynchronizationStatus)status {300if (status == TCHChannelSynchronizationStatusAll) {301[self loadMessages];302dispatch_async(dispatch_get_main_queue(), ^{303[self.tableView reloadData];304[self setViewOnHold:NO];305});306}307}308309#pragma mark - Actions310311- (IBAction)actionButtonTouched:(UIBarButtonItem *)sender {312[self leaveChannel];313}314315- (IBAction)revealButtonTouched:(UIBarButtonItem *)sender {316[self.revealViewController revealToggleAnimated:YES];317}318319@end320
We've actually got a real chat app going here, but let's make it more interesting with multiple channels.
The application uses SWRevealViewController 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"1011@interface MainChatViewController ()12@property (weak, nonatomic) IBOutlet UIBarButtonItem *revealButtonItem;13@property (weak, nonatomic) IBOutlet UIBarButtonItem *actionButtonItem;1415@property (strong, nonatomic) NSMutableOrderedSet *messages;1617@end1819static NSString * const TWCChatCellIdentifier = @"ChatTableCell";20static NSString * const TWCChatStatusCellIdentifier = @"ChatStatusTableCell";2122static NSString * const TWCOpenGeneralChannelSegue = @"OpenGeneralChat";23static NSInteger const TWCLabelTag = 200;2425@implementation MainChatViewController2627#pragma mark Initialization2829- (void)viewDidLoad {30[super viewDidLoad];3132if (self.revealViewController)33{34[self.revealButtonItem setTarget: self.revealViewController];35[self.revealButtonItem setAction: @selector( revealToggle: )];36[self.navigationController.navigationBar addGestureRecognizer: self.revealViewController.panGestureRecognizer];37self.revealViewController.rearViewRevealOverdraw = 0.f;38}3940self.bounces = YES;41self.shakeToClearEnabled = YES;42self.keyboardPanningEnabled = YES;43self.shouldScrollToBottomAfterKeyboardShows = NO;44self.inverted = YES;4546UINib *cellNib = [UINib nibWithNibName:TWCChatCellIdentifier bundle:nil];47[self.tableView registerNib:cellNib48forCellReuseIdentifier:TWCChatCellIdentifier];4950UINib *cellStatusNib = [UINib nibWithNibName:TWCChatStatusCellIdentifier bundle:nil];51[self.tableView registerNib:cellStatusNib52forCellReuseIdentifier:TWCChatStatusCellIdentifier];5354self.textInputbar.autoHideRightButton = YES;55self.textInputbar.maxCharCount = 256;56self.textInputbar.counterStyle = SLKCounterStyleSplit;57self.textInputbar.counterPosition = SLKCounterPositionTop;5859UIFont *font = [UIFont fontWithName:@"Avenir-Light" size:14];60self.textView.font = font;6162[self.rightButton setTitleColor:[UIColor colorWithRed:0.973 green:0.557 blue:0.502 alpha:1]63forState:UIControlStateNormal];6465font = [UIFont fontWithName:@"Avenir-Heavy" size:17];66self.navigationController.navigationBar.titleTextAttributes = @{NSFontAttributeName:font};6768self.tableView.allowsSelection = NO;69self.tableView.estimatedRowHeight = 70;70self.tableView.rowHeight = UITableViewAutomaticDimension;71self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;7273if (!self.channel) {74id generalChannel = [ChannelManager sharedManager].generalChannel;75if (generalChannel) {76self.channel = generalChannel;77} else {78[[ChannelManager sharedManager] joinGeneralChatRoomWithCompletion:^(BOOL succeeded) {79if (succeeded) {80self.channel = [ChannelManager sharedManager].generalChannel;81}82}];83}84}85}8687- (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}9394- (void)viewDidAppear:(BOOL)animated {95[super viewDidAppear:animated];96[self scrollToBottomMessage];97}9899- (NSMutableOrderedSet *)messages {100if (!_messages) {101_messages = [[NSMutableOrderedSet alloc] init];102}103return _messages;104}105106- (void)setChannel:(TCHChannel *)channel {107if ([channel isKindOfClass:[TCHChannelDescriptor class]]) {108TCHChannelDescriptor *channelDescriptor = (TCHChannelDescriptor*)channel;109[channelDescriptor channelWithCompletion:^(TCHResult *success, TCHChannel *channel) {110if (success) {111[self actuallySetChannel:channel];112}113}];114} else {115[self actuallySetChannel:channel];116}117}118119- (void)actuallySetChannel:(TCHChannel *)channel {120_channel = channel;121self.title = self.channel.friendlyName;122self.channel.delegate = self;123124if (self.channel == [ChannelManager sharedManager].generalChannel) {125self.navigationItem.rightBarButtonItem = nil;126}127128[self setViewOnHold:YES];129130if (self.channel.status != TCHChannelStatusJoined) {131[self.channel joinWithCompletion:^(TCHResult* result) {132NSLog(@"%@", @"Channel Joined");133[self setViewOnHold:NO];134}];135}136if (self.channel.synchronizationStatus == TCHChannelSynchronizationStatusAll) {137[self loadMessages];138[self setViewOnHold:NO];139}140}141142// Disable user input and show activity indicator143- (void)setViewOnHold:(BOOL)onHold {144self.textInputbarHidden = onHold;145[UIApplication sharedApplication].networkActivityIndicatorVisible = onHold;146}147148- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {149return 1;150}151152- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {153return self.messages.count;154}155156- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {157UITableViewCell *cell = nil;158159id message = [self.messages objectAtIndex:indexPath.row];160161if ([message isKindOfClass:[TCHMessage class]]) {162cell = [self getChatCellForTableView:tableView forIndexPath:indexPath message:message];163}164else {165cell = [self getStatusCellForTableView:tableView forIndexPath:indexPath message:message];166}167168cell.transform = tableView.transform;169return cell;170}171172- (ChatTableCell *)getChatCellForTableView:(UITableView *)tableView173forIndexPath:(NSIndexPath *)indexPath174message:(TCHMessage *)message {175UITableViewCell *cell = [self.tableView176dequeueReusableCellWithIdentifier:TWCChatCellIdentifier forIndexPath:indexPath];177178ChatTableCell *chatCell = (ChatTableCell *)cell;179chatCell.user = message.author;180chatCell.date = [[[DateTodayFormatter alloc] init]181stringFromDate:[NSDate dateWithISO8601String:message.timestamp]];182183chatCell.message = message.body;184185return chatCell;186}187188- (UITableViewCell *)getStatusCellForTableView:(UITableView *)tableView189forIndexPath:(NSIndexPath *)indexPath190message:(StatusEntry *)message {191UITableViewCell *cell = [self.tableView192dequeueReusableCellWithIdentifier:TWCChatStatusCellIdentifier forIndexPath:indexPath];193194UILabel *label = [cell viewWithTag:TWCLabelTag];195label.text = [NSString stringWithFormat:@"User %@ has %@",196message.member.identity, (message.status == TWCMemberStatusJoined) ? @"joined" : @"left"];197198return cell;199}200201- (void)didPressRightButton:(id)sender {202[self.textView refreshFirstResponder];203[self sendMessage: [self.textView.text copy]];204[super didPressRightButton:sender];205}206207#pragma mark Chat Service208- (void)sendMessage: (NSString *)inputMessage {209TCHMessageOptions *messageOptions = [[[TCHMessageOptions alloc] init] withBody:inputMessage];210[self.channel.messages sendMessageWithOptions:messageOptions211completion:nil];212}213214215216- (void)addMessages:(NSArray *)messages {217[self.messages addObjectsFromArray:messages];218[self sortMessages];219dispatch_async(dispatch_get_main_queue(), ^{220[self.tableView reloadData];221if (self.messages.count > 0) {222[self scrollToBottomMessage];223}224});225}226227228- (void)sortMessages {229[self.messages sortUsingDescriptors:@[[[NSSortDescriptor alloc]230initWithKey:@"timestamp" ascending:NO]]];231}232233- (void)scrollToBottomMessage {234if (self.messages.count == 0) {235return;236}237238NSIndexPath *bottomMessageIndex = [NSIndexPath indexPathForRow:0239inSection:0];240[self.tableView scrollToRowAtIndexPath:bottomMessageIndex241atScrollPosition:UITableViewScrollPositionBottom animated:NO];242}243244- (void)loadMessages {245[self.messages removeAllObjects];246if (self.channel.synchronizationStatus == TCHChannelSynchronizationStatusAll) {247[self.channel.messages248getLastMessagesWithCount:100249completion:^(TCHResult *result, NSArray *messages) {250if ([result isSuccessful]) {251[self addMessages: messages];252}253}];254}255}256257- (void)leaveChannel {258[self.channel leaveWithCompletion:^(TCHResult* result) {259if ([result isSuccessful]) {260[(MenuViewController *)self.revealViewController.rearViewController deselectSelectedChannel];261[self.revealViewController.rearViewController262performSegueWithIdentifier:TWCOpenGeneralChannelSegue sender:nil];263}264}];265}266267#pragma mark - TMMessageDelegate268269- (void)chatClient:(TwilioChatClient *)client270channel:(TCHChannel *)channel271messageAdded:(TCHMessage *)message {272if (![self.messages containsObject:message]) {273[self addMessages:@[message]];274}275}276277- (void)chatClient:(TwilioChatClient *)client278channelDeleted:(TCHChannel *)channel {279dispatch_async(dispatch_get_main_queue(), ^{280if (channel == self.channel) {281[self.revealViewController.rearViewController282performSegueWithIdentifier:TWCOpenGeneralChannelSegue sender:nil];283}284});285}286287- (void)chatClient:(TwilioChatClient *)client288channel:(TCHChannel *)channel289memberJoined:(TCHMember *)member {290[self addMessages:@[[StatusEntry statusEntryWithMember:member status:TWCMemberStatusJoined]]];291}292293- (void)chatClient:(TwilioChatClient *)client294channel:(TCHChannel *)channel295memberLeft:(TCHMember *)member {296[self addMessages:@[[StatusEntry statusEntryWithMember:member status:TWCMemberStatusLeft]]];297}298299- (void)chatClient:(TwilioChatClient *)client channel:(TCHChannel *)channel synchronizationStatusChanged:(TCHChannelSynchronizationStatus)status {300if (status == TCHChannelSynchronizationStatusAll) {301[self loadMessages];302dispatch_async(dispatch_get_main_queue(), ^{303[self.tableView reloadData];304[self setViewOnHold:NO];305});306}307}308309#pragma mark - Actions310311- (IBAction)actionButtonTouched:(UIBarButtonItem *)sender {312[self leaveChannel];313}314315- (IBAction)revealButtonTouched:(UIBarButtonItem *)sender {316[self.revealViewController revealToggleAnimated:YES];317}318319@end320
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"34#define _ Underscore56@interface ChannelManager ()7@property (strong, nonatomic) TCHChannel *generalChannel;8@end910static NSString * const TWCDefaultChannelUniqueName = @"general";11static NSString * const TWCDefaultChannelName = @"General Channel";1213static NSString * const TWCFriendlyNameKey = @"friendlyName";1415@implementation ChannelManager1617+ (instancetype)sharedManager {18static ChannelManager *sharedMyManager = nil;19static dispatch_once_t onceToken;20dispatch_once(&onceToken, ^{21sharedMyManager = [[self alloc] init];22});23return sharedMyManager;24}2526- (instancetype)init {27self.channels = [[NSMutableOrderedSet alloc] init];28return self;29}3031#pragma mark General channel3233- (void)joinGeneralChatRoomWithCompletion:(SucceedHandler)completion {34[self.channelsList channelWithSidOrUniqueName:TWCDefaultChannelUniqueName completion:^(TCHResult *result, TCHChannel *channel) {35if ([result isSuccessful]) {36self.generalChannel = channel;37}3839if (self.generalChannel) {40[self joinGeneralChatRoomWithUniqueName:nil completion:completion];41}42else {43[self createGeneralChatRoomWithCompletion:^(BOOL succeeded) {44if (succeeded) {45[self joinGeneralChatRoomWithUniqueName:TWCDefaultChannelUniqueName completion:completion];46return;47}48if (completion) completion(NO);49}];50};51}];52}5354- (void)joinGeneralChatRoomWithUniqueName:(NSString *)uniqueName completion:(SucceedHandler)completion {55[self.generalChannel joinWithCompletion:^(TCHResult *result) {56if ([result isSuccessful]) {57if (uniqueName) {58[self setGeneralChatRoomUniqueNameWithCompletion:completion];59return;60}61}62if (completion) completion([result isSuccessful]);63}];64}6566- (void)createGeneralChatRoomWithCompletion:(SucceedHandler)completion {67NSDictionary *options = [68NSDictionary69dictionaryWithObjectsAndKeys:TWCDefaultChannelName,70TCHChannelOptionFriendlyName,71TCHChannelTypePublic,72TCHChannelOptionType,73nil74];7576[self.channelsList createChannelWithOptions:options77completion:^(TCHResult *result, TCHChannel *channel) {78if ([result isSuccessful]) {79self.generalChannel = channel;80}81if (completion) completion([result isSuccessful]);82}];83}8485- (void)setGeneralChatRoomUniqueNameWithCompletion:(SucceedHandler)completion {86[self.generalChannel setUniqueName:TWCDefaultChannelUniqueName87completion:^(TCHResult *result) {88if (completion) completion([result isSuccessful]);89}];90}9192#pragma mark Populate channels9394- (void)populateChannels {95self.channels = [[NSMutableOrderedSet alloc] init];96[self.channelsList userChannelDescriptorsWithCompletion:^(TCHResult * _Nonnull result, TCHChannelDescriptorPaginator * _Nullable channelPaginator) {97[self.channels addObjectsFromArray:[channelPaginator items]];98[self sortAndDedupeChannels];99if (self.delegate) {100[self.delegate reloadChannelList];101}102}];103104[self.channelsList publicChannelDescriptorsWithCompletion:^(TCHResult *result,105TCHChannelDescriptorPaginator *channelDescPaginator) {106[self.channels addObjectsFromArray: [channelDescPaginator items]];107[self sortAndDedupeChannels];108if (self.delegate) {109[self.delegate reloadChannelList];110}111}];112}113114- (void)sortAndDedupeChannels {115NSMutableDictionary *channelsDict = [[NSMutableDictionary alloc] init];116117for(TCHChannel *channel in self.channels) {118if (![channelsDict objectForKey: channel.sid] ||119![[channelsDict objectForKey: channel.sid] isKindOfClass: [NSNull class]]) {120[channelsDict setObject:channel forKey:channel.sid];121}122}123124NSMutableOrderedSet *dedupedChannels = [NSMutableOrderedSet125orderedSetWithArray:[channelsDict allValues]];126127SEL sortSelector = @selector(localizedCaseInsensitiveCompare:);128129NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:TWCFriendlyNameKey130ascending:YES131selector:sortSelector];132133[dedupedChannels sortUsingDescriptors:@[descriptor]];134135self.channels = dedupedChannels;136}137138# pragma mark Create channel139140- (void)createChannelWithName:(NSString *)name completion:(ChannelHandler)completion {141if ([name isEqualToString:TWCDefaultChannelName]) {142if (completion) completion(NO, nil);143return;144}145146NSDictionary *options = [147NSDictionary148dictionaryWithObjectsAndKeys:name,149TCHChannelOptionFriendlyName,150TCHChannelTypePublic,151TCHChannelOptionType,152nil153];154[self.channelsList155createChannelWithOptions:options156completion:^(TCHResult *result, TCHChannel *channel) {157[self.channels addObject:channel];158[self sortAndDedupeChannels];159if (completion) completion([result isSuccessful], channel);160}];161}162163# pragma mark TwilioChatClientDelegate164165- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel{166dispatch_async(dispatch_get_main_queue(), ^{167[self.channels addObject:channel];168[self sortAndDedupeChannels];169[self.delegate chatClient:client channelAdded:channel];170});171}172173- (void)chatClient:(TwilioChatClient *)client channel:(nonnull TCHChannel *)channel updated:(TCHChannelUpdate)updated {174dispatch_async(dispatch_get_main_queue(), ^{175[self.delegate chatClient:client channel:channel updated:updated];176});177}178179- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {180dispatch_async(dispatch_get_main_queue(), ^{181[[ChannelManager sharedManager].channels removeObject:channel];182[self.delegate chatClient:client channelDeleted:channel];183});184}185186- (void)chatClient:(TwilioChatClient *)client synchronizationStatusUpdated:(TCHClientSynchronizationStatus)status {187188}189190@end191
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
.
twiliochat/MenuViewController.m
1#import <SWRevealViewController/SWRevealViewController.h>2#import "MenuViewController.h"3#import "MenuTableCell.h"4#import "InputDialogController.h"5#import "MainChatViewController.h"6#import "MessagingManager.h"7#import "AlertDialogController.h"8#import "ChannelManager.h"9#import "SessionManager.h"1011@interface MenuViewController ()12@property (weak, nonatomic) IBOutlet UILabel *usernameLabel;13@property (weak, nonatomic) IBOutlet UITableView *tableView;14@property (strong, nonatomic) UIRefreshControl *refreshControl;1516@property (strong, nonatomic) TCHChannel *recentlyAddedChannel;17@end1819static NSString * const TWCOpenChannelSegue = @"OpenChat";20static NSInteger const TWCRefreshControlXOffset = 120;212223@implementation MenuViewController2425#pragma mark Initialization2627- (void)viewDidLoad {28[super viewDidLoad];2930UIImageView *bgImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"home-bg"]];31bgImage.frame = self.tableView.frame;32self.tableView.backgroundView = bgImage;3334self.usernameLabel.text = [SessionManager getUsername];3536self.refreshControl = [[UIRefreshControl alloc] init];37[self.tableView addSubview:self.refreshControl];38[self.refreshControl addTarget:self39action:@selector(refreshChannels)40forControlEvents:UIControlEventValueChanged];41self.refreshControl.tintColor = [UIColor whiteColor];4243CGRect frame = self.refreshControl.frame;44frame.origin.x = CGRectGetMinX(frame) - TWCRefreshControlXOffset;45self.refreshControl.frame = frame;4647[ChannelManager sharedManager].delegate = self;48[self reloadChannelList];49}5051#pragma mark - Table view data source5253- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {54if (![ChannelManager sharedManager].channels) {55return 1;56}5758return [ChannelManager sharedManager].channels.count;59}6061- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {62UITableViewCell *cell = nil;6364if (![ChannelManager sharedManager].channels) {65cell = [self loadingCellForTableView:tableView];66}67else {68cell = [self channelCellForTableView:tableView atIndexPath:indexPath];69}70[cell layoutIfNeeded];7172return cell;73}7475#pragma mark - Table view delegate7677- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {78TCHChannel *channel = [[ChannelManager sharedManager].channels objectAtIndex:indexPath.row];79return channel != [ChannelManager sharedManager].generalChannel;80}818283- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {84if (editingStyle == UITableViewCellEditingStyleDelete) {85TCHChannel *channel = [[ChannelManager sharedManager].channels objectAtIndex:indexPath.row];86[channel destroyWithCompletion:^(TCHResult *result) {87if ([result isSuccessful]) {88[tableView reloadData];89}90else {91[AlertDialogController showAlertWithMessage:@"You can not delete this channel" title:nil presenter:self];92}93}];94}95}9697- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {98[self performSegueWithIdentifier:TWCOpenChannelSegue sender:indexPath];99}100101#pragma mark - Internal methods102103- (UITableViewCell *)loadingCellForTableView:(UITableView *)tableView {104return [tableView dequeueReusableCellWithIdentifier:@"loadingCell"];105}106107- (UITableViewCell *)channelCellForTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {108MenuTableCell *menuCell = (MenuTableCell *)[tableView dequeueReusableCellWithIdentifier:@"channelCell" forIndexPath:indexPath];109110TCHChannel *channel = [[ChannelManager sharedManager].channels objectAtIndex:indexPath.row];111NSString *friendlyName = channel.friendlyName;112if (channel.friendlyName.length == 0) {113friendlyName = @"(no friendly name)";114}115menuCell.channelName = friendlyName;116117return menuCell;118}119120- (void)reloadChannelList {121[self.tableView reloadData];122[self.refreshControl endRefreshing];123}124125- (void)refreshChannels {126[self.refreshControl beginRefreshing];127[self reloadChannelList];128}129130- (void)deselectSelectedChannel {131NSIndexPath *selectedRow = [self.tableView indexPathForSelectedRow];132133if (selectedRow) {134[self.tableView deselectRowAtIndexPath:selectedRow animated:YES];135}136}137138#pragma mark - Channel139140- (void)createNewChannelDialog {141[InputDialogController showWithTitle:@"New Channel"142message:@"Enter a name for this channel."143placeholder:@"Name"144presenter:self handler:^(NSString *text) {145[[ChannelManager sharedManager] createChannelWithName:text completion:^(BOOL success, TCHChannel *channel) {146if (success) {147[self refreshChannels];148}149}];150}];151}152153#pragma mark - TwilioChatClientDelegate delegate154155- (void)chatClient:(TwilioChatClient *)client channelAdded:(TCHChannel *)channel {156[self.tableView reloadData];157}158159- (void)chatClient:(TwilioChatClient *)client channelChanged:(TCHChannel *)channel {160[self.tableView reloadData];161}162163- (void)chatClient:(TwilioChatClient *)client channelDeleted:(TCHChannel *)channel {164[self.tableView reloadData];165}166167#pragma mark - Logout168169- (void)promtpLogout {170UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil171message:@"You are about to Logout." preferredStyle:UIAlertControllerStyleAlert];172173UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel"174style:UIAlertActionStyleCancel handler:nil];175176UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"Confirm"177style:UIAlertActionStyleDefault178handler:^(UIAlertAction *action) {179[self logOut];180}];181182[alert addAction:cancelAction];183[alert addAction:confirmAction];184[self presentViewController:alert animated:YES completion:nil];185}186187- (void)logOut {188[[MessagingManager sharedManager] logout];189[[MessagingManager sharedManager] presentRootViewController];190}191192#pragma mark Actions193194- (IBAction)logoutButtonTouched:(UIButton *)sender {195[self promtpLogout];196}197198- (IBAction)newChannelButtonTouched:(UIButton *)sender {199[self createNewChannelDialog];200}201202#pragma mark - Navigation203204- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {205if ([segue.identifier isEqualToString:TWCOpenChannelSegue]) {206NSIndexPath *indexPath = (NSIndexPath *)sender;207208TCHChannel *channel = [[ChannelManager sharedManager].channels objectAtIndex:indexPath.row];209UINavigationController *navigationController = [segue destinationViewController];210MainChatViewController *chatViewController = (MainChatViewController *)[navigationController visibleViewController];211chatViewController.channel = channel;212}213}214215#pragma mark Style216217- (UIStatusBarStyle)preferredStatusBarStyle {218return UIStatusBarStyleLightContent;219}220221222@end223
That's it! We've built an iOS application with Objective-C. 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:
Twilio Notifications for iOS Quickstart using Swift
Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think.