Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Chat with iOS and Swift


(error)

Danger

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

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

(warning)

Warning

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

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

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

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

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

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


Initialize the Programmable Chat Client

initialize-the-programmable-chat-client page anchor

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

(information)

Info

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

Fetch Access Token

fetch-access-token page anchor

twiliochat/MessagingManager.swift


_192
import UIKit
_192
_192
class 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
_192
extension 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.


Synchronize the Programmable Chat Client

synchronize-the-programmable-chat-client page anchor

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

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

Synchronize the Chat Client

synchronize-the-chat-client page anchor

twiliochat/MessagingManager.swift


_192
import UIKit
_192
_192
class 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
_192
extension 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.


Get the Channel Descriptor List

get-the-channel-descriptor-list page anchor

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

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

twiliochat/ChannelManager.swift


_172
import UIKit
_172
_172
protocol ChannelManagerDelegate {
_172
func reloadChannelDescriptorList()
_172
}
_172
_172
class 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
_172
extension 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(link takes you to an external page) or channelDeleted on our application. Given the creation or deletion of a channel, we'll reload the channel list in the reveal controller. If a channel is deleted and we were currently joined to that channel, the application will automatically join the general channel.

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

Listen for Client Events

listen-for-client-events page anchor

twiliochat/ChannelManager.swift


_172
import UIKit
_172
_172
protocol ChannelManagerDelegate {
_172
func reloadChannelDescriptorList()
_172
}
_172
_172
class 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
_172
extension 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.


Join the General Channel

join-the-general-channel page anchor

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

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

Join or Create a General Channel

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

twiliochat/ChannelManager.swift


_172
import UIKit
_172
_172
protocol ChannelManagerDelegate {
_172
func reloadChannelDescriptorList()
_172
}
_172
_172
class 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
_172
extension 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.


Listen to Channel Events

listen-to-channel-events page anchor

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

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

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

twiliochat/MainChatViewController.swift


_263
import UIKit
_263
import SlackTextViewController
_263
_263
class 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
_263
extension 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(link takes you to an external page) to show a sidebar that contains a list of the channels created for that Twilio account.

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

twiliochat/MainChatViewController.swift


_263
import UIKit
_263
import SlackTextViewController
_263
_263
class 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
_263
extension 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


_172
import UIKit
_172
_172
protocol ChannelManagerDelegate {
_172
func reloadChannelDescriptorList()
_172
}
_172
_172
class 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
_172
extension 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


_193
import UIKit
_193
_193
class 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
_193
extension 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
_193
extension 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
_193
extension 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:

Notifications Quickstart(link takes you to an external page)

Twilio Notifications for iOS Quickstart using Swift

Did this help?

did-this-help page anchor

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


Rate this page: