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.
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.
Ready to implement a chat application using Twilio 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 as required.
The only thing you need to create a client is an access token. This token holds information about your Twilio account and Programmable Chat API keys. We have created a web version of Twilio chat in different languages. You can use any of these to generate the token:
You will need to set up your access token URL in the Keys.plist file in the resources folder. The default is http://localhost:8000/token - you may need to change this. For instance, if you set up the Node.js version of the chat server (listed above) - this URL would be http://localhost:3000/token
twiliochat/MessagingManager.swift
_192import UIKit_192_192class MessagingManager: NSObject {_192_192 static let _sharedManager = MessagingManager()_192_192 var client:TwilioChatClient?_192 var delegate:ChannelManager?_192 var connected = false_192_192 var userIdentity:String {_192 return SessionManager.getUsername()_192 }_192_192 var hasIdentity: Bool {_192 return SessionManager.isLoggedIn()_192 }_192_192 override init() {_192 super.init()_192 delegate = ChannelManager.sharedManager_192 }_192_192 class func sharedManager() -> MessagingManager {_192 return _sharedManager_192 }_192_192 func presentRootViewController() {_192 if (!hasIdentity) {_192 presentViewControllerByName(viewController: "LoginViewController")_192 return_192 }_192_192 if (!connected) {_192 connectClientWithCompletion { success, error in_192 print("Delegate method will load views when sync is complete")_192 if (!success || error != nil) {_192 DispatchQueue.main.async {_192 self.presentViewControllerByName(viewController: "LoginViewController")_192 }_192 }_192 }_192 return_192 }_192_192 presentViewControllerByName(viewController: "RevealViewController")_192 }_192_192 func presentViewControllerByName(viewController: String) {_192 presentViewController(controller: storyBoardWithName(name: "Main").instantiateViewController(withIdentifier: viewController))_192 }_192_192 func presentLaunchScreen() {_192 presentViewController(controller: storyBoardWithName(name: "LaunchScreen").instantiateInitialViewController()!)_192 }_192_192 func presentViewController(controller: UIViewController) {_192 let window = UIApplication.shared.delegate!.window!!_192 window.rootViewController = controller_192 }_192_192 func storyBoardWithName(name:String) -> UIStoryboard {_192 return UIStoryboard(name:name, bundle: Bundle.main)_192 }_192_192 // MARK: User and session management_192_192 func loginWithUsername(username: String,_192 completion: @escaping (Bool, NSError?) -> Void) {_192 SessionManager.loginWithUsername(username: username)_192 connectClientWithCompletion(completion: completion)_192 }_192_192 func logout() {_192 SessionManager.logout()_192 DispatchQueue.global(qos: .userInitiated).async {_192 self.client?.shutdown()_192 self.client = nil_192 }_192 self.connected = false_192 }_192_192 // MARK: Twilio client_192_192 func loadGeneralChatRoomWithCompletion(completion:@escaping (Bool, NSError?) -> Void) {_192 ChannelManager.sharedManager.joinGeneralChatRoomWithCompletion { succeeded in_192 if succeeded {_192 completion(succeeded, nil)_192 }_192 else {_192 let error = self.errorWithDescription(description: "Could not join General channel", code: 300)_192 completion(succeeded, error)_192 }_192 }_192 }_192_192 func connectClientWithCompletion(completion: @escaping (Bool, NSError?) -> Void) {_192 if (client != nil) {_192 logout()_192 }_192_192 requestTokenWithCompletion { succeeded, token in_192 if let token = token, succeeded {_192 self.initializeClientWithToken(token: token)_192 completion(succeeded, nil)_192 }_192 else {_192 let error = self.errorWithDescription(description: "Could not get access token", code:301)_192 completion(succeeded, error)_192 }_192 }_192 }_192_192 func initializeClientWithToken(token: String) {_192 DispatchQueue.main.async {_192 UIApplication.shared.isNetworkActivityIndicatorVisible = true_192 }_192 TwilioChatClient.chatClient(withToken: token, properties: nil, delegate: self) { [weak self] result, chatClient in_192 guard (result.isSuccessful()) else { return }_192_192 UIApplication.shared.isNetworkActivityIndicatorVisible = true_192 self?.connected = true_192 self?.client = chatClient_192 }_192 }_192_192 func requestTokenWithCompletion(completion:@escaping (Bool, String?) -> Void) {_192 if let device = UIDevice.current.identifierForVendor?.uuidString {_192 TokenRequestHandler.fetchToken(params: ["device": device, "identity":SessionManager.getUsername()]) {response,error in_192 var token: String?_192 token = response["token"] as? String_192 completion(token != nil, token)_192 }_192 }_192 }_192_192 func errorWithDescription(description: String, code: Int) -> NSError {_192 let userInfo = [NSLocalizedDescriptionKey : description]_192 return NSError(domain: "app", code: code, userInfo: userInfo)_192 }_192}_192_192// MARK: - TwilioChatClientDelegate_192extension MessagingManager : TwilioChatClientDelegate {_192 func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {_192 self.delegate?.chatClient(client, channelAdded: channel)_192 }_192_192 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {_192 self.delegate?.chatClient(client, channel: channel, updated: updated)_192 }_192_192 func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {_192 self.delegate?.chatClient(client, channelDeleted: channel)_192 }_192_192 func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {_192 if status == TCHClientSynchronizationStatus.completed {_192 UIApplication.shared.isNetworkActivityIndicatorVisible = false_192 ChannelManager.sharedManager.channelsList = client.channelsList()_192 ChannelManager.sharedManager.populateChannelDescriptors()_192 loadGeneralChatRoomWithCompletion { success, error in_192 if success {_192 self.presentRootViewController()_192 }_192 }_192 }_192 self.delegate?.chatClient(client, synchronizationStatusUpdated: status)_192 }_192_192 func chatClientTokenWillExpire(_ client: TwilioChatClient) {_192 requestTokenWithCompletion { succeeded, token in_192 if (succeeded) {_192 client.updateToken(token!)_192 }_192 else {_192 print("Error while trying to get new access token")_192 }_192 }_192 }_192_192 func chatClientTokenExpired(_ client: TwilioChatClient) {_192 requestTokenWithCompletion { succeeded, token in_192 if (succeeded) {_192 client.updateToken(token!)_192 }_192 else {_192 print("Error while trying to get new access token")_192 }_192 }_192 }_192}
Now it's time to synchronize your Twilio client.
The synchronizationStatusChanged
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 (next step). Otherwise, calling client.channelsList() will return nil
.
twiliochat/MessagingManager.swift
_192import UIKit_192_192class MessagingManager: NSObject {_192_192 static let _sharedManager = MessagingManager()_192_192 var client:TwilioChatClient?_192 var delegate:ChannelManager?_192 var connected = false_192_192 var userIdentity:String {_192 return SessionManager.getUsername()_192 }_192_192 var hasIdentity: Bool {_192 return SessionManager.isLoggedIn()_192 }_192_192 override init() {_192 super.init()_192 delegate = ChannelManager.sharedManager_192 }_192_192 class func sharedManager() -> MessagingManager {_192 return _sharedManager_192 }_192_192 func presentRootViewController() {_192 if (!hasIdentity) {_192 presentViewControllerByName(viewController: "LoginViewController")_192 return_192 }_192_192 if (!connected) {_192 connectClientWithCompletion { success, error in_192 print("Delegate method will load views when sync is complete")_192 if (!success || error != nil) {_192 DispatchQueue.main.async {_192 self.presentViewControllerByName(viewController: "LoginViewController")_192 }_192 }_192 }_192 return_192 }_192_192 presentViewControllerByName(viewController: "RevealViewController")_192 }_192_192 func presentViewControllerByName(viewController: String) {_192 presentViewController(controller: storyBoardWithName(name: "Main").instantiateViewController(withIdentifier: viewController))_192 }_192_192 func presentLaunchScreen() {_192 presentViewController(controller: storyBoardWithName(name: "LaunchScreen").instantiateInitialViewController()!)_192 }_192_192 func presentViewController(controller: UIViewController) {_192 let window = UIApplication.shared.delegate!.window!!_192 window.rootViewController = controller_192 }_192_192 func storyBoardWithName(name:String) -> UIStoryboard {_192 return UIStoryboard(name:name, bundle: Bundle.main)_192 }_192_192 // MARK: User and session management_192_192 func loginWithUsername(username: String,_192 completion: @escaping (Bool, NSError?) -> Void) {_192 SessionManager.loginWithUsername(username: username)_192 connectClientWithCompletion(completion: completion)_192 }_192_192 func logout() {_192 SessionManager.logout()_192 DispatchQueue.global(qos: .userInitiated).async {_192 self.client?.shutdown()_192 self.client = nil_192 }_192 self.connected = false_192 }_192_192 // MARK: Twilio client_192_192 func loadGeneralChatRoomWithCompletion(completion:@escaping (Bool, NSError?) -> Void) {_192 ChannelManager.sharedManager.joinGeneralChatRoomWithCompletion { succeeded in_192 if succeeded {_192 completion(succeeded, nil)_192 }_192 else {_192 let error = self.errorWithDescription(description: "Could not join General channel", code: 300)_192 completion(succeeded, error)_192 }_192 }_192 }_192_192 func connectClientWithCompletion(completion: @escaping (Bool, NSError?) -> Void) {_192 if (client != nil) {_192 logout()_192 }_192_192 requestTokenWithCompletion { succeeded, token in_192 if let token = token, succeeded {_192 self.initializeClientWithToken(token: token)_192 completion(succeeded, nil)_192 }_192 else {_192 let error = self.errorWithDescription(description: "Could not get access token", code:301)_192 completion(succeeded, error)_192 }_192 }_192 }_192_192 func initializeClientWithToken(token: String) {_192 DispatchQueue.main.async {_192 UIApplication.shared.isNetworkActivityIndicatorVisible = true_192 }_192 TwilioChatClient.chatClient(withToken: token, properties: nil, delegate: self) { [weak self] result, chatClient in_192 guard (result.isSuccessful()) else { return }_192_192 UIApplication.shared.isNetworkActivityIndicatorVisible = true_192 self?.connected = true_192 self?.client = chatClient_192 }_192 }_192_192 func requestTokenWithCompletion(completion:@escaping (Bool, String?) -> Void) {_192 if let device = UIDevice.current.identifierForVendor?.uuidString {_192 TokenRequestHandler.fetchToken(params: ["device": device, "identity":SessionManager.getUsername()]) {response,error in_192 var token: String?_192 token = response["token"] as? String_192 completion(token != nil, token)_192 }_192 }_192 }_192_192 func errorWithDescription(description: String, code: Int) -> NSError {_192 let userInfo = [NSLocalizedDescriptionKey : description]_192 return NSError(domain: "app", code: code, userInfo: userInfo)_192 }_192}_192_192// MARK: - TwilioChatClientDelegate_192extension MessagingManager : TwilioChatClientDelegate {_192 func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {_192 self.delegate?.chatClient(client, channelAdded: channel)_192 }_192_192 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {_192 self.delegate?.chatClient(client, channel: channel, updated: updated)_192 }_192_192 func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {_192 self.delegate?.chatClient(client, channelDeleted: channel)_192 }_192_192 func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {_192 if status == TCHClientSynchronizationStatus.completed {_192 UIApplication.shared.isNetworkActivityIndicatorVisible = false_192 ChannelManager.sharedManager.channelsList = client.channelsList()_192 ChannelManager.sharedManager.populateChannelDescriptors()_192 loadGeneralChatRoomWithCompletion { success, error in_192 if success {_192 self.presentRootViewController()_192 }_192 }_192 }_192 self.delegate?.chatClient(client, synchronizationStatusUpdated: status)_192 }_192_192 func chatClientTokenWillExpire(_ client: TwilioChatClient) {_192 requestTokenWithCompletion { succeeded, token in_192 if (succeeded) {_192 client.updateToken(token!)_192 }_192 else {_192 print("Error while trying to get new access token")_192 }_192 }_192 }_192_192 func chatClientTokenExpired(_ client: TwilioChatClient) {_192 requestTokenWithCompletion { succeeded, token in_192 if (succeeded) {_192 client.updateToken(token!)_192 }_192 else {_192 print("Error while trying to get new access token")_192 }_192 }_192 }_192}
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 TCHChannels to our ChannelManager
.
Now we will get a list of light-weight channel descriptors to use for the list of channels in our application. We combine the channels the user has subscribed to (both public and private) with the list of publicly available channels. We do need to merge this list, and avoid adding duplicates. We also sort the channel list here alphabetically by the friendly name.
twiliochat/ChannelManager.swift
_172import UIKit_172_172protocol ChannelManagerDelegate {_172 func reloadChannelDescriptorList()_172}_172_172class ChannelManager: NSObject {_172 static let sharedManager = ChannelManager()_172_172 static let defaultChannelUniqueName = "general"_172 static let defaultChannelName = "General Channel"_172_172 var delegate:ChannelManagerDelegate?_172_172 var channelsList:TCHChannels?_172 var channelDescriptors:NSOrderedSet?_172 var generalChannel:TCHChannel!_172_172 override init() {_172 super.init()_172 channelDescriptors = NSMutableOrderedSet()_172 }_172_172 // MARK: - General channel_172_172 func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {_172_172 let uniqueName = ChannelManager.defaultChannelUniqueName_172 if let channelsList = self.channelsList {_172 channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in_172 self.generalChannel = channel_172_172 if self.generalChannel != nil {_172 self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)_172 } else {_172 self.createGeneralChatRoomWithCompletion { succeeded in_172 if (succeeded) {_172 self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)_172 return_172 }_172_172 completion(false)_172 }_172 }_172 }_172 }_172 }_172_172 func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {_172 generalChannel.join { result in_172 if ((result.isSuccessful()) && name != nil) {_172 self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)_172 return_172 }_172 completion((result.isSuccessful()))_172 }_172 }_172_172 func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {_172 let channelName = ChannelManager.defaultChannelName_172 let options = [_172 TCHChannelOptionFriendlyName: channelName,_172 TCHChannelOptionType: TCHChannelType.public.rawValue_172 ] as [String : Any]_172 channelsList!.createChannel(options: options) { result, channel in_172 if (result.isSuccessful()) {_172 self.generalChannel = channel_172 }_172 completion((result.isSuccessful()))_172 }_172 }_172_172 func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {_172 generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in_172 completion((result.isSuccessful()))_172 }_172 }_172_172 // MARK: - Populate channel Descriptors_172_172 func populateChannelDescriptors() {_172_172 channelsList?.userChannelDescriptors { result, paginator in_172 guard let paginator = paginator else {_172 return_172 }_172_172 let newChannelDescriptors = NSMutableOrderedSet()_172 newChannelDescriptors.addObjects(from: paginator.items())_172 self.channelsList?.publicChannelDescriptors { result, paginator in_172 guard let paginator = paginator else {_172 return_172 }_172_172 // de-dupe channel list_172 let channelIds = NSMutableSet()_172 for descriptor in newChannelDescriptors {_172 if let descriptor = descriptor as? TCHChannelDescriptor {_172 if let sid = descriptor.sid {_172 channelIds.add(sid)_172 }_172 }_172 }_172 for descriptor in paginator.items() {_172 if let sid = descriptor.sid {_172 if !channelIds.contains(sid) {_172 channelIds.add(sid)_172 newChannelDescriptors.add(descriptor)_172 }_172 }_172 }_172_172_172 // sort the descriptors_172 let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))_172 let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)_172 newChannelDescriptors.sort(using: [descriptor])_172_172 self.channelDescriptors = newChannelDescriptors_172_172 if let delegate = self.delegate {_172 delegate.reloadChannelDescriptorList()_172 }_172 }_172 }_172 }_172_172_172 // MARK: - Create channel_172_172 func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {_172 if (name == ChannelManager.defaultChannelName) {_172 completion(false, nil)_172 return_172 }_172_172 let channelOptions = [_172 TCHChannelOptionFriendlyName: name,_172 TCHChannelOptionType: TCHChannelType.public.rawValue_172 ] as [String : Any]_172 UIApplication.shared.isNetworkActivityIndicatorVisible = true;_172 self.channelsList?.createChannel(options: channelOptions) { result, channel in_172 UIApplication.shared.isNetworkActivityIndicatorVisible = false_172 completion((result.isSuccessful()), channel)_172 }_172 }_172}_172_172// MARK: - TwilioChatClientDelegate_172extension ChannelManager : TwilioChatClientDelegate {_172 func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {_172 DispatchQueue.main.async {_172 self.populateChannelDescriptors()_172 }_172 }_172_172 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {_172 DispatchQueue.main.async {_172 self.delegate?.reloadChannelDescriptorList()_172 }_172 }_172_172 func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {_172 DispatchQueue.main.async {_172 self.populateChannelDescriptors()_172 }_172_172 }_172_172 func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {_172 }_172}
Let's see how we can listen to events from the chat client so we can update our app's state.
The Programmable Chat Client will trigger events such as channelAdded
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.swift
_172import UIKit_172_172protocol ChannelManagerDelegate {_172 func reloadChannelDescriptorList()_172}_172_172class ChannelManager: NSObject {_172 static let sharedManager = ChannelManager()_172_172 static let defaultChannelUniqueName = "general"_172 static let defaultChannelName = "General Channel"_172_172 var delegate:ChannelManagerDelegate?_172_172 var channelsList:TCHChannels?_172 var channelDescriptors:NSOrderedSet?_172 var generalChannel:TCHChannel!_172_172 override init() {_172 super.init()_172 channelDescriptors = NSMutableOrderedSet()_172 }_172_172 // MARK: - General channel_172_172 func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {_172_172 let uniqueName = ChannelManager.defaultChannelUniqueName_172 if let channelsList = self.channelsList {_172 channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in_172 self.generalChannel = channel_172_172 if self.generalChannel != nil {_172 self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)_172 } else {_172 self.createGeneralChatRoomWithCompletion { succeeded in_172 if (succeeded) {_172 self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)_172 return_172 }_172_172 completion(false)_172 }_172 }_172 }_172 }_172 }_172_172 func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {_172 generalChannel.join { result in_172 if ((result.isSuccessful()) && name != nil) {_172 self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)_172 return_172 }_172 completion((result.isSuccessful()))_172 }_172 }_172_172 func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {_172 let channelName = ChannelManager.defaultChannelName_172 let options = [_172 TCHChannelOptionFriendlyName: channelName,_172 TCHChannelOptionType: TCHChannelType.public.rawValue_172 ] as [String : Any]_172 channelsList!.createChannel(options: options) { result, channel in_172 if (result.isSuccessful()) {_172 self.generalChannel = channel_172 }_172 completion((result.isSuccessful()))_172 }_172 }_172_172 func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {_172 generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in_172 completion((result.isSuccessful()))_172 }_172 }_172_172 // MARK: - Populate channel Descriptors_172_172 func populateChannelDescriptors() {_172_172 channelsList?.userChannelDescriptors { result, paginator in_172 guard let paginator = paginator else {_172 return_172 }_172_172 let newChannelDescriptors = NSMutableOrderedSet()_172 newChannelDescriptors.addObjects(from: paginator.items())_172 self.channelsList?.publicChannelDescriptors { result, paginator in_172 guard let paginator = paginator else {_172 return_172 }_172_172 // de-dupe channel list_172 let channelIds = NSMutableSet()_172 for descriptor in newChannelDescriptors {_172 if let descriptor = descriptor as? TCHChannelDescriptor {_172 if let sid = descriptor.sid {_172 channelIds.add(sid)_172 }_172 }_172 }_172 for descriptor in paginator.items() {_172 if let sid = descriptor.sid {_172 if !channelIds.contains(sid) {_172 channelIds.add(sid)_172 newChannelDescriptors.add(descriptor)_172 }_172 }_172 }_172_172_172 // sort the descriptors_172 let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))_172 let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)_172 newChannelDescriptors.sort(using: [descriptor])_172_172 self.channelDescriptors = newChannelDescriptors_172_172 if let delegate = self.delegate {_172 delegate.reloadChannelDescriptorList()_172 }_172 }_172 }_172 }_172_172_172 // MARK: - Create channel_172_172 func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {_172 if (name == ChannelManager.defaultChannelName) {_172 completion(false, nil)_172 return_172 }_172_172 let channelOptions = [_172 TCHChannelOptionFriendlyName: name,_172 TCHChannelOptionType: TCHChannelType.public.rawValue_172 ] as [String : Any]_172 UIApplication.shared.isNetworkActivityIndicatorVisible = true;_172 self.channelsList?.createChannel(options: channelOptions) { result, channel in_172 UIApplication.shared.isNetworkActivityIndicatorVisible = false_172 completion((result.isSuccessful()), channel)_172 }_172 }_172}_172_172// MARK: - TwilioChatClientDelegate_172extension ChannelManager : TwilioChatClientDelegate {_172 func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {_172 DispatchQueue.main.async {_172 self.populateChannelDescriptors()_172 }_172 }_172_172 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {_172 DispatchQueue.main.async {_172 self.delegate?.reloadChannelDescriptorList()_172 }_172 }_172_172 func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {_172 DispatchQueue.main.async {_172 self.populateChannelDescriptors()_172 }_172_172 }_172_172 func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {_172 }_172}
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 Programmable Chat client allows you to create private channels and handle invitations.
Once you have joined a channel, you can register a class as the TCHChannelDelegate
so you can start listening to events such as messageAdded
or memberJoined
. We'll show you how to do this in the next step.
twiliochat/ChannelManager.swift
_172import UIKit_172_172protocol ChannelManagerDelegate {_172 func reloadChannelDescriptorList()_172}_172_172class ChannelManager: NSObject {_172 static let sharedManager = ChannelManager()_172_172 static let defaultChannelUniqueName = "general"_172 static let defaultChannelName = "General Channel"_172_172 var delegate:ChannelManagerDelegate?_172_172 var channelsList:TCHChannels?_172 var channelDescriptors:NSOrderedSet?_172 var generalChannel:TCHChannel!_172_172 override init() {_172 super.init()_172 channelDescriptors = NSMutableOrderedSet()_172 }_172_172 // MARK: - General channel_172_172 func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {_172_172 let uniqueName = ChannelManager.defaultChannelUniqueName_172 if let channelsList = self.channelsList {_172 channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in_172 self.generalChannel = channel_172_172 if self.generalChannel != nil {_172 self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)_172 } else {_172 self.createGeneralChatRoomWithCompletion { succeeded in_172 if (succeeded) {_172 self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)_172 return_172 }_172_172 completion(false)_172 }_172 }_172 }_172 }_172 }_172_172 func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {_172 generalChannel.join { result in_172 if ((result.isSuccessful()) && name != nil) {_172 self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)_172 return_172 }_172 completion((result.isSuccessful()))_172 }_172 }_172_172 func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {_172 let channelName = ChannelManager.defaultChannelName_172 let options = [_172 TCHChannelOptionFriendlyName: channelName,_172 TCHChannelOptionType: TCHChannelType.public.rawValue_172 ] as [String : Any]_172 channelsList!.createChannel(options: options) { result, channel in_172 if (result.isSuccessful()) {_172 self.generalChannel = channel_172 }_172 completion((result.isSuccessful()))_172 }_172 }_172_172 func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {_172 generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in_172 completion((result.isSuccessful()))_172 }_172 }_172_172 // MARK: - Populate channel Descriptors_172_172 func populateChannelDescriptors() {_172_172 channelsList?.userChannelDescriptors { result, paginator in_172 guard let paginator = paginator else {_172 return_172 }_172_172 let newChannelDescriptors = NSMutableOrderedSet()_172 newChannelDescriptors.addObjects(from: paginator.items())_172 self.channelsList?.publicChannelDescriptors { result, paginator in_172 guard let paginator = paginator else {_172 return_172 }_172_172 // de-dupe channel list_172 let channelIds = NSMutableSet()_172 for descriptor in newChannelDescriptors {_172 if let descriptor = descriptor as? TCHChannelDescriptor {_172 if let sid = descriptor.sid {_172 channelIds.add(sid)_172 }_172 }_172 }_172 for descriptor in paginator.items() {_172 if let sid = descriptor.sid {_172 if !channelIds.contains(sid) {_172 channelIds.add(sid)_172 newChannelDescriptors.add(descriptor)_172 }_172 }_172 }_172_172_172 // sort the descriptors_172 let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))_172 let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)_172 newChannelDescriptors.sort(using: [descriptor])_172_172 self.channelDescriptors = newChannelDescriptors_172_172 if let delegate = self.delegate {_172 delegate.reloadChannelDescriptorList()_172 }_172 }_172 }_172 }_172_172_172 // MARK: - Create channel_172_172 func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {_172 if (name == ChannelManager.defaultChannelName) {_172 completion(false, nil)_172 return_172 }_172_172 let channelOptions = [_172 TCHChannelOptionFriendlyName: name,_172 TCHChannelOptionType: TCHChannelType.public.rawValue_172 ] as [String : Any]_172 UIApplication.shared.isNetworkActivityIndicatorVisible = true;_172 self.channelsList?.createChannel(options: channelOptions) { result, channel in_172 UIApplication.shared.isNetworkActivityIndicatorVisible = false_172 completion((result.isSuccessful()), channel)_172 }_172 }_172}_172_172// MARK: - TwilioChatClientDelegate_172extension ChannelManager : TwilioChatClientDelegate {_172 func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {_172 DispatchQueue.main.async {_172 self.populateChannelDescriptors()_172 }_172 }_172_172 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {_172 DispatchQueue.main.async {_172 self.delegate?.reloadChannelDescriptorList()_172 }_172 }_172_172 func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {_172 DispatchQueue.main.async {_172 self.populateChannelDescriptors()_172 }_172_172 }_172_172 func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {_172 }_172}
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:
messageAdded
: When someone sends a message to the channel you are connected to.
channelDeleted
: When someone deletes a channel.
memberJoined
: When someone joins the channel.
memberLeft
: When someone leaves the channel.
synchronizationStatusChanged
: When channel synchronization status changes.
As you may have noticed, each one of these methods includes useful objects as parameters. One example is the actual message that was added to the channel.
twiliochat/MainChatViewController.swift
_263import UIKit_263import SlackTextViewController_263_263class MainChatViewController: SLKTextViewController {_263 static let TWCChatCellIdentifier = "ChatTableCell"_263 static let TWCChatStatusCellIdentifier = "ChatStatusTableCell"_263_263 static let TWCOpenGeneralChannelSegue = "OpenGeneralChat"_263 static let TWCLabelTag = 200_263_263 var _channel:TCHChannel!_263 var channel:TCHChannel! {_263 get {_263 return _channel_263 }_263 set(channel) {_263 _channel = channel_263 title = _channel.friendlyName_263 _channel.delegate = self_263_263 if _channel == ChannelManager.sharedManager.generalChannel {_263 navigationItem.rightBarButtonItem = nil_263 }_263_263 joinChannel()_263 }_263 }_263_263 var messages:Set<TCHMessage> = Set<TCHMessage>()_263 var sortedMessages:[TCHMessage]!_263_263 @IBOutlet weak var revealButtonItem: UIBarButtonItem!_263 @IBOutlet weak var actionButtonItem: UIBarButtonItem!_263_263 override func viewDidLoad() {_263 super.viewDidLoad()_263_263 if (revealViewController() != nil) {_263 revealButtonItem.target = revealViewController()_263 revealButtonItem.action = #selector(SWRevealViewController.revealToggle(_:))_263 navigationController?.navigationBar.addGestureRecognizer(revealViewController().panGestureRecognizer())_263 revealViewController().rearViewRevealOverdraw = 0_263 }_263_263 bounces = true_263 shakeToClearEnabled = true_263 isKeyboardPanningEnabled = true_263 shouldScrollToBottomAfterKeyboardShows = false_263 isInverted = true_263_263 let cellNib = UINib(nibName: MainChatViewController.TWCChatCellIdentifier, bundle: nil)_263 tableView!.register(cellNib, forCellReuseIdentifier:MainChatViewController.TWCChatCellIdentifier)_263_263 let cellStatusNib = UINib(nibName: MainChatViewController.TWCChatStatusCellIdentifier, bundle: nil)_263 tableView!.register(cellStatusNib, forCellReuseIdentifier:MainChatViewController.TWCChatStatusCellIdentifier)_263_263 textInputbar.autoHideRightButton = true_263 textInputbar.maxCharCount = 256_263 textInputbar.counterStyle = .split_263 textInputbar.counterPosition = .top_263_263 let font = UIFont(name:"Avenir-Light", size:14)_263 textView.font = font_263_263 rightButton.setTitleColor(UIColor(red:0.973, green:0.557, blue:0.502, alpha:1), for: .normal)_263_263 if let font = UIFont(name:"Avenir-Heavy", size:17) {_263 navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: font]_263 }_263_263 tableView!.allowsSelection = false_263 tableView!.estimatedRowHeight = 70_263 tableView!.rowHeight = UITableView.automaticDimension_263 tableView!.separatorStyle = .none_263_263 if channel == nil {_263 channel = ChannelManager.sharedManager.generalChannel_263 }_263 }_263_263 override func viewDidLayoutSubviews() {_263 super.viewDidLayoutSubviews()_263_263 // required for iOS 11_263 textInputbar.bringSubviewToFront(textInputbar.textView)_263 textInputbar.bringSubviewToFront(textInputbar.leftButton)_263 textInputbar.bringSubviewToFront(textInputbar.rightButton)_263_263 }_263_263 override func viewDidAppear(_ animated: Bool) {_263 super.viewDidAppear(animated)_263 scrollToBottom()_263 }_263_263 override func numberOfSections(in tableView: UITableView) -> Int {_263 return 1_263 }_263_263 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: NSInteger) -> Int {_263 return messages.count_263 }_263_263 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {_263 var cell:UITableViewCell_263_263 let message = sortedMessages[indexPath.row]_263_263 if let statusMessage = message as? StatusMessage {_263 cell = getStatusCellForTableView(tableView: tableView, forIndexPath:indexPath, message:statusMessage)_263 }_263 else {_263 cell = getChatCellForTableView(tableView: tableView, forIndexPath:indexPath, message:message)_263 }_263_263 cell.transform = tableView.transform_263 return cell_263 }_263_263 func getChatCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: TCHMessage) -> UITableViewCell {_263 let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatCellIdentifier, for:indexPath as IndexPath)_263_263 let chatCell: ChatTableCell = cell as! ChatTableCell_263 let date = NSDate.dateWithISO8601String(dateString: message.dateCreated ?? "")_263 let timestamp = DateTodayFormatter().stringFromDate(date: date)_263_263 chatCell.setUser(user: message.author ?? "[Unknown author]", message: message.body, date: timestamp ?? "[Unknown date]")_263_263 return chatCell_263 }_263_263 func getStatusCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: StatusMessage) -> UITableViewCell {_263 let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatStatusCellIdentifier, for:indexPath as IndexPath)_263_263 let label = cell.viewWithTag(MainChatViewController.TWCLabelTag) as! UILabel_263 let memberStatus = (message.status! == .Joined) ? "joined" : "left"_263 label.text = "User \(message.statusMember.identity ?? "[Unknown user]") has \(memberStatus)"_263 return cell_263 }_263_263 func joinChannel() {_263 setViewOnHold(onHold: true)_263_263 if channel.status != .joined {_263 channel.join { result in_263 print("Channel Joined")_263 }_263 return_263 }_263_263 loadMessages()_263 setViewOnHold(onHold: false)_263 }_263_263 // Disable user input and show activity indicator_263 func setViewOnHold(onHold: Bool) {_263 self.isTextInputbarHidden = onHold;_263 UIApplication.shared.isNetworkActivityIndicatorVisible = onHold;_263 }_263_263 override func didPressRightButton(_ sender: Any!) {_263 textView.refreshFirstResponder()_263 sendMessage(inputMessage: textView.text)_263 super.didPressRightButton(sender)_263 }_263_263 // MARK: - Chat Service_263_263 func sendMessage(inputMessage: String) {_263 let messageOptions = TCHMessageOptions().withBody(inputMessage)_263 channel.messages?.sendMessage(with: messageOptions, completion: nil)_263 }_263_263 func addMessages(newMessages:Set<TCHMessage>) {_263 messages = messages.union(newMessages)_263 sortMessages()_263 DispatchQueue.main.async {_263 self.tableView!.reloadData()_263 if self.messages.count > 0 {_263 self.scrollToBottom()_263 }_263 }_263 }_263_263 func sortMessages() {_263 sortedMessages = messages.sorted { (a, b) -> Bool in_263 (a.dateCreated ?? "") > (b.dateCreated ?? "")_263 }_263 }_263_263 func loadMessages() {_263 messages.removeAll()_263 if channel.synchronizationStatus == .all {_263 channel.messages?.getLastWithCount(100) { (result, items) in_263 self.addMessages(newMessages: Set(items!))_263 }_263 }_263 }_263_263 func scrollToBottom() {_263 if messages.count > 0 {_263 let indexPath = IndexPath(row: 0, section: 0)_263 tableView!.scrollToRow(at: indexPath, at: .bottom, animated: true)_263 }_263 }_263_263 func leaveChannel() {_263 channel.leave { result in_263 if (result.isSuccessful()) {_263 let menuViewController = self.revealViewController().rearViewController as! MenuViewController_263 menuViewController.deselectSelectedChannel()_263 self.revealViewController().rearViewController.performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)_263 }_263 }_263 }_263_263 // MARK: - Actions_263_263 @IBAction func actionButtonTouched(_ sender: UIBarButtonItem) {_263 leaveChannel()_263 }_263_263 @IBAction func revealButtonTouched(_ sender: AnyObject) {_263 revealViewController().revealToggle(animated: true)_263 }_263}_263_263extension MainChatViewController : TCHChannelDelegate {_263 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, messageAdded message: TCHMessage) {_263 if !messages.contains(message) {_263 addMessages(newMessages: [message])_263 }_263 }_263_263 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberJoined member: TCHMember) {_263 addMessages(newMessages: [StatusMessage(statusMember:member, status:.Joined)])_263 }_263_263 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberLeft member: TCHMember) {_263 addMessages(newMessages: [StatusMessage(statusMember:member, status:.Left)])_263 }_263_263 func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {_263 DispatchQueue.main.async {_263 if channel == self.channel {_263 self.revealViewController().rearViewController_263 .performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)_263 }_263 }_263 }_263_263 func chatClient(_ client: TwilioChatClient,_263 channel: TCHChannel,_263 synchronizationStatusUpdated status: TCHChannelSynchronizationStatus) {_263 if status == .all {_263 loadMessages()_263 DispatchQueue.main.async {_263 self.tableView?.reloadData()_263 self.setViewOnHold(onHold: false)_263 }_263 }_263 }_263}
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 joinChannel
method takes care of joining to the selected channel and loading the messages.
twiliochat/MainChatViewController.swift
_263import UIKit_263import SlackTextViewController_263_263class MainChatViewController: SLKTextViewController {_263 static let TWCChatCellIdentifier = "ChatTableCell"_263 static let TWCChatStatusCellIdentifier = "ChatStatusTableCell"_263_263 static let TWCOpenGeneralChannelSegue = "OpenGeneralChat"_263 static let TWCLabelTag = 200_263_263 var _channel:TCHChannel!_263 var channel:TCHChannel! {_263 get {_263 return _channel_263 }_263 set(channel) {_263 _channel = channel_263 title = _channel.friendlyName_263 _channel.delegate = self_263_263 if _channel == ChannelManager.sharedManager.generalChannel {_263 navigationItem.rightBarButtonItem = nil_263 }_263_263 joinChannel()_263 }_263 }_263_263 var messages:Set<TCHMessage> = Set<TCHMessage>()_263 var sortedMessages:[TCHMessage]!_263_263 @IBOutlet weak var revealButtonItem: UIBarButtonItem!_263 @IBOutlet weak var actionButtonItem: UIBarButtonItem!_263_263 override func viewDidLoad() {_263 super.viewDidLoad()_263_263 if (revealViewController() != nil) {_263 revealButtonItem.target = revealViewController()_263 revealButtonItem.action = #selector(SWRevealViewController.revealToggle(_:))_263 navigationController?.navigationBar.addGestureRecognizer(revealViewController().panGestureRecognizer())_263 revealViewController().rearViewRevealOverdraw = 0_263 }_263_263 bounces = true_263 shakeToClearEnabled = true_263 isKeyboardPanningEnabled = true_263 shouldScrollToBottomAfterKeyboardShows = false_263 isInverted = true_263_263 let cellNib = UINib(nibName: MainChatViewController.TWCChatCellIdentifier, bundle: nil)_263 tableView!.register(cellNib, forCellReuseIdentifier:MainChatViewController.TWCChatCellIdentifier)_263_263 let cellStatusNib = UINib(nibName: MainChatViewController.TWCChatStatusCellIdentifier, bundle: nil)_263 tableView!.register(cellStatusNib, forCellReuseIdentifier:MainChatViewController.TWCChatStatusCellIdentifier)_263_263 textInputbar.autoHideRightButton = true_263 textInputbar.maxCharCount = 256_263 textInputbar.counterStyle = .split_263 textInputbar.counterPosition = .top_263_263 let font = UIFont(name:"Avenir-Light", size:14)_263 textView.font = font_263_263 rightButton.setTitleColor(UIColor(red:0.973, green:0.557, blue:0.502, alpha:1), for: .normal)_263_263 if let font = UIFont(name:"Avenir-Heavy", size:17) {_263 navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: font]_263 }_263_263 tableView!.allowsSelection = false_263 tableView!.estimatedRowHeight = 70_263 tableView!.rowHeight = UITableView.automaticDimension_263 tableView!.separatorStyle = .none_263_263 if channel == nil {_263 channel = ChannelManager.sharedManager.generalChannel_263 }_263 }_263_263 override func viewDidLayoutSubviews() {_263 super.viewDidLayoutSubviews()_263_263 // required for iOS 11_263 textInputbar.bringSubviewToFront(textInputbar.textView)_263 textInputbar.bringSubviewToFront(textInputbar.leftButton)_263 textInputbar.bringSubviewToFront(textInputbar.rightButton)_263_263 }_263_263 override func viewDidAppear(_ animated: Bool) {_263 super.viewDidAppear(animated)_263 scrollToBottom()_263 }_263_263 override func numberOfSections(in tableView: UITableView) -> Int {_263 return 1_263 }_263_263 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: NSInteger) -> Int {_263 return messages.count_263 }_263_263 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {_263 var cell:UITableViewCell_263_263 let message = sortedMessages[indexPath.row]_263_263 if let statusMessage = message as? StatusMessage {_263 cell = getStatusCellForTableView(tableView: tableView, forIndexPath:indexPath, message:statusMessage)_263 }_263 else {_263 cell = getChatCellForTableView(tableView: tableView, forIndexPath:indexPath, message:message)_263 }_263_263 cell.transform = tableView.transform_263 return cell_263 }_263_263 func getChatCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: TCHMessage) -> UITableViewCell {_263 let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatCellIdentifier, for:indexPath as IndexPath)_263_263 let chatCell: ChatTableCell = cell as! ChatTableCell_263 let date = NSDate.dateWithISO8601String(dateString: message.dateCreated ?? "")_263 let timestamp = DateTodayFormatter().stringFromDate(date: date)_263_263 chatCell.setUser(user: message.author ?? "[Unknown author]", message: message.body, date: timestamp ?? "[Unknown date]")_263_263 return chatCell_263 }_263_263 func getStatusCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: StatusMessage) -> UITableViewCell {_263 let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatStatusCellIdentifier, for:indexPath as IndexPath)_263_263 let label = cell.viewWithTag(MainChatViewController.TWCLabelTag) as! UILabel_263 let memberStatus = (message.status! == .Joined) ? "joined" : "left"_263 label.text = "User \(message.statusMember.identity ?? "[Unknown user]") has \(memberStatus)"_263 return cell_263 }_263_263 func joinChannel() {_263 setViewOnHold(onHold: true)_263_263 if channel.status != .joined {_263 channel.join { result in_263 print("Channel Joined")_263 }_263 return_263 }_263_263 loadMessages()_263 setViewOnHold(onHold: false)_263 }_263_263 // Disable user input and show activity indicator_263 func setViewOnHold(onHold: Bool) {_263 self.isTextInputbarHidden = onHold;_263 UIApplication.shared.isNetworkActivityIndicatorVisible = onHold;_263 }_263_263 override func didPressRightButton(_ sender: Any!) {_263 textView.refreshFirstResponder()_263 sendMessage(inputMessage: textView.text)_263 super.didPressRightButton(sender)_263 }_263_263 // MARK: - Chat Service_263_263 func sendMessage(inputMessage: String) {_263 let messageOptions = TCHMessageOptions().withBody(inputMessage)_263 channel.messages?.sendMessage(with: messageOptions, completion: nil)_263 }_263_263 func addMessages(newMessages:Set<TCHMessage>) {_263 messages = messages.union(newMessages)_263 sortMessages()_263 DispatchQueue.main.async {_263 self.tableView!.reloadData()_263 if self.messages.count > 0 {_263 self.scrollToBottom()_263 }_263 }_263 }_263_263 func sortMessages() {_263 sortedMessages = messages.sorted { (a, b) -> Bool in_263 (a.dateCreated ?? "") > (b.dateCreated ?? "")_263 }_263 }_263_263 func loadMessages() {_263 messages.removeAll()_263 if channel.synchronizationStatus == .all {_263 channel.messages?.getLastWithCount(100) { (result, items) in_263 self.addMessages(newMessages: Set(items!))_263 }_263 }_263 }_263_263 func scrollToBottom() {_263 if messages.count > 0 {_263 let indexPath = IndexPath(row: 0, section: 0)_263 tableView!.scrollToRow(at: indexPath, at: .bottom, animated: true)_263 }_263 }_263_263 func leaveChannel() {_263 channel.leave { result in_263 if (result.isSuccessful()) {_263 let menuViewController = self.revealViewController().rearViewController as! MenuViewController_263 menuViewController.deselectSelectedChannel()_263 self.revealViewController().rearViewController.performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)_263 }_263 }_263 }_263_263 // MARK: - Actions_263_263 @IBAction func actionButtonTouched(_ sender: UIBarButtonItem) {_263 leaveChannel()_263 }_263_263 @IBAction func revealButtonTouched(_ sender: AnyObject) {_263 revealViewController().revealToggle(animated: true)_263 }_263}_263_263extension MainChatViewController : TCHChannelDelegate {_263 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, messageAdded message: TCHMessage) {_263 if !messages.contains(message) {_263 addMessages(newMessages: [message])_263 }_263 }_263_263 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberJoined member: TCHMember) {_263 addMessages(newMessages: [StatusMessage(statusMember:member, status:.Joined)])_263 }_263_263 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberLeft member: TCHMember) {_263 addMessages(newMessages: [StatusMessage(statusMember:member, status:.Left)])_263 }_263_263 func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {_263 DispatchQueue.main.async {_263 if channel == self.channel {_263 self.revealViewController().rearViewController_263 .performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)_263 }_263 }_263 }_263_263 func chatClient(_ client: TwilioChatClient,_263 channel: TCHChannel,_263 synchronizationStatusUpdated status: TCHChannelSynchronizationStatus) {_263 if status == .all {_263 loadMessages()_263 DispatchQueue.main.async {_263 self.tableView?.reloadData()_263 self.setViewOnHold(onHold: false)_263 }_263 }_263 }_263}
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 is as simple as calling createChannel
and passing a dictionary with the new channel information.
twiliochat/ChannelManager.swift
_172import UIKit_172_172protocol ChannelManagerDelegate {_172 func reloadChannelDescriptorList()_172}_172_172class ChannelManager: NSObject {_172 static let sharedManager = ChannelManager()_172_172 static let defaultChannelUniqueName = "general"_172 static let defaultChannelName = "General Channel"_172_172 var delegate:ChannelManagerDelegate?_172_172 var channelsList:TCHChannels?_172 var channelDescriptors:NSOrderedSet?_172 var generalChannel:TCHChannel!_172_172 override init() {_172 super.init()_172 channelDescriptors = NSMutableOrderedSet()_172 }_172_172 // MARK: - General channel_172_172 func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {_172_172 let uniqueName = ChannelManager.defaultChannelUniqueName_172 if let channelsList = self.channelsList {_172 channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in_172 self.generalChannel = channel_172_172 if self.generalChannel != nil {_172 self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)_172 } else {_172 self.createGeneralChatRoomWithCompletion { succeeded in_172 if (succeeded) {_172 self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)_172 return_172 }_172_172 completion(false)_172 }_172 }_172 }_172 }_172 }_172_172 func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {_172 generalChannel.join { result in_172 if ((result.isSuccessful()) && name != nil) {_172 self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)_172 return_172 }_172 completion((result.isSuccessful()))_172 }_172 }_172_172 func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {_172 let channelName = ChannelManager.defaultChannelName_172 let options = [_172 TCHChannelOptionFriendlyName: channelName,_172 TCHChannelOptionType: TCHChannelType.public.rawValue_172 ] as [String : Any]_172 channelsList!.createChannel(options: options) { result, channel in_172 if (result.isSuccessful()) {_172 self.generalChannel = channel_172 }_172 completion((result.isSuccessful()))_172 }_172 }_172_172 func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {_172 generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in_172 completion((result.isSuccessful()))_172 }_172 }_172_172 // MARK: - Populate channel Descriptors_172_172 func populateChannelDescriptors() {_172_172 channelsList?.userChannelDescriptors { result, paginator in_172 guard let paginator = paginator else {_172 return_172 }_172_172 let newChannelDescriptors = NSMutableOrderedSet()_172 newChannelDescriptors.addObjects(from: paginator.items())_172 self.channelsList?.publicChannelDescriptors { result, paginator in_172 guard let paginator = paginator else {_172 return_172 }_172_172 // de-dupe channel list_172 let channelIds = NSMutableSet()_172 for descriptor in newChannelDescriptors {_172 if let descriptor = descriptor as? TCHChannelDescriptor {_172 if let sid = descriptor.sid {_172 channelIds.add(sid)_172 }_172 }_172 }_172 for descriptor in paginator.items() {_172 if let sid = descriptor.sid {_172 if !channelIds.contains(sid) {_172 channelIds.add(sid)_172 newChannelDescriptors.add(descriptor)_172 }_172 }_172 }_172_172_172 // sort the descriptors_172 let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))_172 let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)_172 newChannelDescriptors.sort(using: [descriptor])_172_172 self.channelDescriptors = newChannelDescriptors_172_172 if let delegate = self.delegate {_172 delegate.reloadChannelDescriptorList()_172 }_172 }_172 }_172 }_172_172_172 // MARK: - Create channel_172_172 func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {_172 if (name == ChannelManager.defaultChannelName) {_172 completion(false, nil)_172 return_172 }_172_172 let channelOptions = [_172 TCHChannelOptionFriendlyName: name,_172 TCHChannelOptionType: TCHChannelType.public.rawValue_172 ] as [String : Any]_172 UIApplication.shared.isNetworkActivityIndicatorVisible = true;_172 self.channelsList?.createChannel(options: channelOptions) { result, channel in_172 UIApplication.shared.isNetworkActivityIndicatorVisible = false_172 completion((result.isSuccessful()), channel)_172 }_172 }_172}_172_172// MARK: - TwilioChatClientDelegate_172extension ChannelManager : TwilioChatClientDelegate {_172 func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {_172 DispatchQueue.main.async {_172 self.populateChannelDescriptors()_172 }_172 }_172_172 func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {_172 DispatchQueue.main.async {_172 self.delegate?.reloadChannelDescriptorList()_172 }_172 }_172_172 func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {_172 DispatchQueue.main.async {_172 self.populateChannelDescriptors()_172 }_172_172 }_172_172 func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {_172 }_172}
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), deleting it is as simple as calling the channel's method destroy
.
twiliochat/MenuViewController.swift
_193import UIKit_193_193class MenuViewController: UIViewController {_193 static let TWCOpenChannelSegue = "OpenChat"_193 static let TWCRefreshControlXOffset: CGFloat = 120_193_193 @IBOutlet weak var tableView: UITableView!_193 @IBOutlet weak var usernameLabel: UILabel!_193_193 var refreshControl: UIRefreshControl!_193_193 override func viewDidLoad() {_193 super.viewDidLoad()_193_193 let bgImage = UIImageView(image: UIImage(named:"home-bg"))_193 bgImage.frame = self.tableView.frame_193 tableView.backgroundView = bgImage_193_193 usernameLabel.text = MessagingManager.sharedManager().userIdentity_193_193 refreshControl = UIRefreshControl()_193 tableView.addSubview(refreshControl)_193 refreshControl.addTarget(self, action: #selector(MenuViewController.refreshChannels), for: .valueChanged)_193 refreshControl.tintColor = UIColor.white_193_193 self.refreshControl.frame.origin.x -= MenuViewController.TWCRefreshControlXOffset_193 ChannelManager.sharedManager.delegate = self_193 tableView.reloadData()_193 }_193_193 // MARK: - Internal methods_193_193 func loadingCellForTableView(tableView: UITableView) -> UITableViewCell {_193 return tableView.dequeueReusableCell(withIdentifier: "loadingCell")!_193 }_193_193 func channelCellForTableView(tableView: UITableView, atIndexPath indexPath: NSIndexPath) -> UITableViewCell {_193 let menuCell = tableView.dequeueReusableCell(withIdentifier: "channelCell", for: indexPath as IndexPath) as! MenuTableCell_193_193 if let channelDescriptor = ChannelManager.sharedManager.channelDescriptors![indexPath.row] as? TCHChannelDescriptor {_193 menuCell.channelName = channelDescriptor.friendlyName ?? "[Unknown channel name]"_193 } else {_193 menuCell.channelName = "[Unknown channel name]"_193 }_193_193 return menuCell_193 }_193_193 @objc func refreshChannels() {_193 refreshControl.beginRefreshing()_193 tableView.reloadData()_193 refreshControl.endRefreshing()_193 }_193_193 func deselectSelectedChannel() {_193 let selectedRow = tableView.indexPathForSelectedRow_193 if let row = selectedRow {_193 tableView.deselectRow(at: row, animated: true)_193 }_193 }_193_193 // MARK: - Channel_193_193 func createNewChannelDialog() {_193 InputDialogController.showWithTitle(title: "New Channel",_193 message: "Enter a name for this channel",_193 placeholder: "Name",_193 presenter: self) { text in_193 ChannelManager.sharedManager.createChannelWithName(name: text, completion: { _,_ in_193 ChannelManager.sharedManager.populateChannelDescriptors()_193 })_193 }_193 }_193_193 // MARK: Logout_193_193 func promptLogout() {_193 let alert = UIAlertController(title: nil, message: "You are about to Logout", preferredStyle: .alert)_193_193 let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)_193 let confirmAction = UIAlertAction(title: "Confirm", style: .default) { action in_193 self.logOut()_193 }_193_193 alert.addAction(cancelAction)_193 alert.addAction(confirmAction)_193 present(alert, animated: true, completion: nil)_193 }_193_193 func logOut() {_193 MessagingManager.sharedManager().logout()_193 MessagingManager.sharedManager().presentRootViewController()_193 }_193_193 // MARK: - Actions_193_193 @IBAction func logoutButtonTouched(_ sender: UIButton) {_193 promptLogout()_193 }_193_193 @IBAction func newChannelButtonTouched(_ sender: UIButton) {_193 createNewChannelDialog()_193 }_193_193 // MARK: - Navigation_193_193 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {_193 if segue.identifier == MenuViewController.TWCOpenChannelSegue {_193 let indexPath = sender as! NSIndexPath_193_193 let channelDescriptor = ChannelManager.sharedManager.channelDescriptors![indexPath.row] as! TCHChannelDescriptor_193 let navigationController = segue.destination as! UINavigationController_193_193 channelDescriptor.channel { (result, channel) in_193 if let channel = channel {_193 (navigationController.visibleViewController as! MainChatViewController).channel = channel_193 }_193 }_193_193 }_193 }_193_193 // MARK: - Style_193_193 override var preferredStatusBarStyle: UIStatusBarStyle {_193 return .lightContent_193 }_193}_193_193// MARK: - UITableViewDataSource_193extension MenuViewController : UITableViewDataSource {_193 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {_193 if let channelDescriptors = ChannelManager.sharedManager.channelDescriptors {_193 print (channelDescriptors.count)_193 return channelDescriptors.count_193 }_193 return 1_193 }_193_193 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {_193 let cell: UITableViewCell_193_193 if ChannelManager.sharedManager.channelDescriptors == nil {_193 cell = loadingCellForTableView(tableView: tableView)_193 }_193 else {_193 cell = channelCellForTableView(tableView: tableView, atIndexPath: indexPath as NSIndexPath)_193 }_193_193 cell.layoutIfNeeded()_193 return cell_193 }_193_193 func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {_193 if let channel = ChannelManager.sharedManager.channelDescriptors?.object(at: indexPath.row) as? TCHChannel {_193 return channel != ChannelManager.sharedManager.generalChannel_193 }_193 return false_193 }_193_193 func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle,_193 forRowAt indexPath: IndexPath) {_193 if editingStyle != .delete {_193 return_193 }_193 if let channel = ChannelManager.sharedManager.channelDescriptors?.object(at: indexPath.row) as? TCHChannel {_193 channel.destroy { result in_193 if (result.isSuccessful()) {_193 tableView.reloadData()_193 }_193 else {_193 AlertDialogController.showAlertWithMessage(message: "You can not delete this channel", title: nil, presenter: self)_193 }_193 }_193 }_193 }_193}_193_193// MARK: - UITableViewDelegate_193extension MenuViewController : UITableViewDelegate {_193 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {_193 tableView.deselectRow(at: indexPath, animated: true)_193 performSegue(withIdentifier: MenuViewController.TWCOpenChannelSegue, sender: indexPath)_193 }_193}_193_193_193// MARK: - ChannelManagerDelegate_193extension MenuViewController : ChannelManagerDelegate {_193 func reloadChannelDescriptorList() {_193 tableView.reloadData()_193 }_193}
That's it! We've built an iOS application with Swift. Now you are more than prepared to set up your own chat application.
If you are an iOS developer working with Twilio, you might want to check out this other project:
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.