In this tutorial, learn how you can build a typing indicator feature in Swift for your iOS app using Pusher to send and show realtime indicators in the UI.
In our previous article we considered How to create a public anonymous iOS chat application. We were able to create the application using Swift and Pusher so the application won’t save state.
In this article, we are going to expand that application and add a typing indicator to the application. If you have not read the previous article, I suggest you do so, but if you do not want to then you can grab the source code to the article here and follow along.
As mentioned earlier, we will be adding a typing indicator to our application. This feature indicates that someone is typing a message on the other end – just like WhatsApp, WeChat or instant messaging clients do.
Open the root directory of the source code you downloaded above, then open the .xcworkspace
file included in the directory; this should launch XCode. Now we already have a storyboard. In the storyboard, we have an entry controller, and this has a button to login anonymously. Clicking the button leads to the navigation controller which in turn loads the ChatViewController
.
Note: To test the application you might need to customize the Pusher application credentials in the
ChatViewController
and theindex.js
file in the web app directory. You will also need to runnode index.js
in the webapp directory to start a local web server.
To make this application do what we need it to do we need to do some new things. First, we will add a new endpoint to the web server application that will trigger Pusher once someone starts typing. We will add a new listener in the application that listens in for when someone is typing and finally we will trigger the new endpoint when someone is entering text into the ‘New message’ field.
Now we want to add an endpoint on the web server that will trigger Pusher events every time someone is typing. Open the index.js
in the webapp
directory on your editor of choice. You can now add the /typing
endpoint to the code as shown below:
1app.post('/typing', function (req, res) { 2 var message = { 3 sender: req.body.sender, 4 text: req.body.sender + " is typing..." 5 }; 6 pusher.trigger('chatroom', 'user_typing', message); 7 res.json({success: 200}) 8})
So now, every time we hit the /typing
endpoint, it should trigger Pusher with the message senderId is typing…
. Great.
The next thing to do would be to trigger Pusher every time the current user is typing on the application. This would basically hit the /typing
endpoint we just created with the username
as the sender
parameter.
To make sure we keep our code DRY, we have refactored the code a little. We have abstracted the part that hits our endpoint into one method called hitEndpoint
and we use that now whenever we want to hit the endpoint.
1var isBusySendingEvent : Bool = false
2
3private func postMessage(name: String, message: String) {
4 let params: Parameters = ["sender": name, "text": message]
5 hitEndpoint(url: ChatViewController.API_ENDPOINT + "/messages", parameters: params)
6}
7
8private func sendIsTypingEvent(forUser: String) {
9 if isBusySendingEvent == false {
10 isBusySendingEvent = true
11 let params: Parameters = ["sender": forUser]
12 hitEndpoint(url: ChatViewController.API_ENDPOINT + "/typing", parameters: params)
13 } else {
14 print("Still sending something")
15 }
16}
17
18private func hitEndpoint(url: String, parameters: Parameters) {
19 Alamofire.request(url, method: .post, parameters: parameters).validate().responseJSON { response in
20 switch response.result {
21 case .success:
22 self.isBusySendingEvent = false
23 // Succeeded, do something
24 print("Succeeded")
25 case .failure(let error):
26 self.isBusySendingEvent = false
27 // Failed, do something
28 print(error)
29 }
30 }
31}
32
33override func textViewDidChange(_ textView: UITextView) {
34 super.textViewDidChange(textView)
35 sendIsTypingEvent(forUser: senderId)
36}
In the sendIsTypingEvent
we have a quick flag that we use to stop the application from sending too many requests, especially if the last one has not been fulfilled. Because we trigger this method every time someone changes something on the text field this check is necessary.
The last piece of the puzzle is adding a listener that picks up when someone else is typing and changes the view controller’s title bar to someone is typing…
. To do this, we would use the subscribe
method on the PusherChannel
object.
1override func viewDidLoad() {
2 super.viewDidLoad()
3
4 let n = Int(arc4random_uniform(1000))
5
6 senderId = "anonymous" + String(n)
7 senderDisplayName = senderId
8
9 inputToolbar.contentView.leftBarButtonItem = nil
10
11 incomingBubble = JSQMessagesBubbleImageFactory().incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
12 outgoingBubble = JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(with: UIColor.jsq_messageBubbleGreen())
13
14 collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
15 collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
16
17 automaticallyScrollsToMostRecentMessage = true
18
19 collectionView?.reloadData()
20 collectionView?.layoutIfNeeded()
21
22 listenForNewMessages()
23
24 isTypingEventLifetime = Timer.scheduledTimer(timeInterval: 2.0,
25 target: self,
26 selector: #selector(isTypingEventExpireAction),
27 userInfo: nil,
28 repeats: true)
29
30}
31
32private func listenForNewMessages() {
33 let options = PusherClientOptions(
34 host: .cluster("PUSHER_CLUSTER")
35 )
36
37 pusher = Pusher(key: "PUSHER_ID", options: options)
38
39 let channel = pusher.subscribe("chatroom")
40
41 channel.bind(eventName: "new_message", callback: { (data: Any?) -> Void in
42 if let data = data as? [String: AnyObject] {
43 let author = data["sender"] as! String
44
45 if author != self.senderId {
46 let text = data["text"] as! String
47 self.addMessage(senderId: author, name: author, text: text)
48 self.finishReceivingMessage(animated: true)
49 }
50 }
51 })
52
53 channel.bind(eventName: "user_typing", callback: { (data: Any?) -> Void in
54 if let data = data as? [String: AnyObject] {
55 let author = data["sender"] as! String
56 if author != self.senderId {
57 let text = data["text"] as! String
58 self.navigationItem.title = text
59 }
60 }
61 })
62
63 pusher.connect()
64}
65
66public func isTypingEventExpireAction() {
67 navigationItem.title = "AnonChat"
68}
Above we made some changes. In the listenForNewMessages
we added a new subscription to the user_typing
event, and in the viewDidLoad
method, we added a timer that just runs on intervals and resets the title of the application. So basically, the subscriber picks up the changes in the event from Pusher, updates the navigation title, then the timer resets the title every x seconds.
With this we have completed our task and we should have the typing indicator feature working.
There are many improvements you can obviously add to make the experience a little more seamless, but this demonstrates how the feature can be implemented easily into your iOS application. The source code to the app is available on GitHub.
Have an idea you want to incorporate or just some feedback? Please leave a comment below and tell us what it is.
In the next article, we are going to see how to add a ‘message delivered’ feature to our chat application. As practice, see if you can implement this yourself.