By now, you did all there is do with your app's UI. You have a way to log in, show contacts and display incoming messages. But one crucial bit is missing: Your app is not connected to anything.
In this final part of the SwiftUI course, you'll breathe life into your app by adding networking to send and receive messages to a web server!
To help you, you'll use CometChat Pro. CometChat Pro is a cross-platform chat SDK that lets you build chat apps that work on iOS, Android and the web at the same time. It takes care of storing, receiving and sending messages to your users' devices. There's no need to deal with servers, backend code or WebSockets.
You can find a link to the finished project code on GitHub.
Before you can use CometChat, you'll first need to install the SDK as a dependency. To do this, you'll use CocoaPods, a dependency manager for iOS.
Note: If you don't have CocoaPods installed, enter the following command in Terminal:
Open Terminal and, using cd, navigate to your project's root folder. (The one that contains the .xcodeproj file.)
Enter the following command:
This will initialize CocoaPods for your project and create a new file called Podfile. Open this file in your favorite text editor, and update its contents to the following:
Save the file and head back to Terminal to enter the following command:
This will install CometChat into your app, and create a new file with the .xcworkspace extension. Close any Xcode projects you might have active, and open the .xcworkspace file. From this point on, you'll use the workspace, instead of the project file, to develop your app.
Now that CometChat is in the app, let's begin using it. First, you'll create a CometChat account — don't worry, it's free!
Head over to CometChat's registration page and sign up. Once you sign up, you'll get taken to a dashboard. Create a new app under Add New App. Select USA as the region and enter CometChat as the app's name. Click the + button to create the app.
After 15 or so seconds, you'll have an up-and-running chat service online! All you need to do is connect to it.
Open your new app's dashboard by clicking Explore. Head over to API Keys in the sidebar. Take a look at the keys names API Key with full access scope. Note down the API Key and App ID — you'll need these in a second.
Back in Xcode, create a new plain Swift file named ChatService.swift. This file will be the bridge between CometChat's SDK and your app. All SDK-related code will go in this file.
Add the following contents to the file:
You declare this class and an enum to hold some necessary constants. These will be your CometChat app ID and API key. Make sure to replace the placeholders with the values you saw earlier in the online dashboard.
Next, add a static function to the class that will initialize CometChat:
This sets up CometChat so that it works with your app. Make sure that the region matches the one you set in the online dashboard (us and eu, respectively).
You'll call this function whenever your app launches. The perfect place for this is in SceneDelegate.swift. Add the following line to the top of scene:
Now CometChat will get initialized when a user starts your app.
There's one more step to finish your setup. To use ChatService, your views need a way to access it. Thankfully, you already created an object that you add to your root view's environment — AppStore.
Head to AppStore.swift and add a new property to the nested AppState struct:
Next, modify the declaration of state to include ChatService:
Now, any view can fetch the ChatService by getting it from the AppStore environment object. Convenient!
Run the app and check the console. You should see a message saying that you connected to CometChat. It lives!
With our setup done, it's time to move onto some real features. We'll start with logging in. You already implemented the UI for this, all that's left is telling CometChat a user wants to log in.
Open up ChatService.swift and add a new property:
This property will store the currently logged in user. Because there's one shared ChatService instance in the environment, all views will have access to the same user.
CometChat has its own User class, but you won't be using this in your app. Instead, you'll add a way to convert CometChat's Users to Contacts.
Open Contact.swift and import CometChat at the top of the file:
Next, add the following extension to the bottom of the file:
This initializes a Contact with a CometChat User instance.
Combine's Futures and Promises
Next, get back to ChatService.swift and add a method to log the user in:
Future is a Combine publisher that will eventually produce a single value. For instance, if you're making a network request to fetch a user, you can represent that value as Future<User, Error>. Right now, there's no value, but in the future, there'll be a user or an error.
Put another way, a Future promises a value. That's why promises and futures are often used interchangeably.
Futures are usually a replacement for callbacks. Before Combine, each network request had a callback function that it would call when the request completes. Instead of returning a value, the functions would receive a callback.
Futures allow you to return a value (a future) right away — even if the data isn't yet loaded. This gives the caller more flexibility. They can get the future and do whatever they want with it: Attach callbacks, perform transformations with map or compactMap, cancel the request mid-way through, ignore the result and so on.
I think it's safe to say futures are the future of asynchronous code in Swift.
Creating a Future
Let's get back to login. Since login returns a Future, you'll have to create one in the function. Add the following to login:
Here, you'll immediately return a Future from the function. A Future is initialized with a closure. This closure has one argument: A function called a promise. The promise is a function that you'll call when you get the data you want to load.
To sum up: Future is a struct that gets initialized with a closure. This closure is a function that takes another function as its argument called a promise. Within the closure, you'll call this promise to push data through the promise to the caller.
It may sound complex, but after a few goes at creating futures, you'll get the hang of it!
Add the code to login inside Future's closure:
Here's what's going on in the closure:
You call CometChat's login function and pass it the user's unique ID (email) and the app's API key.
If CometChat manages to log the user in, you'll convert the user to a Contact and set the property you created earlier to store the user.
After dispatching back to the main queue, you'll call promise with a successful value. This notifies anyone listening to the Future that new data has arrived.
On the other hand, if something goes wrong, you'll call promise with an unsuccessful value, giving anyone listening an error.
Now you can hook up your login UI with the function you just created. Open up LoginView.swift and add an import to the top of the file:
Next, add the following property to the struct:
You've already learned about cancellables. Here, you declare a set to hold all of LoginView's subscriptions. Since LoginView is a struct (and thus immutable), you'll need to mark the set as @State before you can add new items to the set.
Next, modify login so that it calls the function you created in ChatService:
login returns a Future, so you call that future's sink method. sink is not specific to futures, though. It's the way you attach closures to Combine Publishers. Whenever the publisher publishes a new event, the closure you provide in sink gets called.
In this case, you provide two closures: One for a failed, and one for a successful result. On failure, you'll print out the error. If the user is logged in successfully, however, you'll set the current user on the AppStore and initiate a transition to the contacts screen. Finally, you'll store the cancellable inside the set you added to the struct.
Build and run the app and try to log in. If you try to use your email you'll get an error. This is expected — your email doesn't exist in the database of users! Thankfully, there are some values you can use to test out your chat app.
CometChat gives you five default users with user IDs superhero1 through superhero5. You can see these users if you navigate to the Users page on CometChat's online dashboard.
Enter superhero1 as the email. This is Iron Man's top-secret username, so don't share it with anyone!
If everything goes correctly, you'll get sent to the contacts screen. This means your app logged in to CometChat! You're successfully communicating with an online chat service.
The contacts you see are still fake, though. Let's take care of that.
Fetching Contacts from CometChat
CometChat stores each registered contact for you. This means you can easily fetch all existing users to display them on the contacts screen.
Head back to ChatService.swift and add the following code to the class:
You create a UsersRequest and store a reference to it. You do this using a UsersRequestBuilder: It allows you to configure your request in different ways. For instance, you can only fetch the user's friends, hide blocked users, paginate the request and so on.
Next, you'll create another future and use the request to fetch all users in your app:
If the request returns successfully, you'll convert each user to a Contact and pass all the users to the caller. Otherwise, if something goes wrong, you'll print out an error.
Let's call this function from ContactsView.swift. First, import Combine in the file:
Next, add a set of cancellables to the struct:
Then, add the following function:
Here, you use sink to respond to the future as you did earlier. In this case, when you get users you'll set them into your state variable, letting the view update itself. If you get an error, you'll print it to the console.
Next, call this method when the view appears by adding the following line to the bottom of body:
Finally, change the declaration of items to remove hard-coded contacts:
You won't be needing these anymore. Run the app, log in as superhero1 and move on to the contacts screen. Instead of seeing hard-coded contacts, you'll see the users CometChat created for you, including Captain America, Spiderman and even a few of the X-Men!
Updating a contact's online status in real-time
Currently, all of your contacts are showing as offline. There are two reasons for that: One, nobody else is using your app yet. But, more importantly, you also haven't implemented a way to track their online status. CometChat tracks this and notifies you when they come online. You'll use that to update your views.
Converting delegates into Combine publishers with passthrough subjects
CometChat uses the delegate pattern to notify you of changes to a contact's online status. This is a useful pattern that is used universally in UIKit. In SwiftUI, we can take advantage of Combine and its declarative nature for code that is more straightforward than delegates. To do this, we'll use Combine's Subjects to convert the delegate methods into Combine publishers.
Back in ChatService.swift, add the following two properties to the top of the class:
You just declared your first subject! Subjects are a special kind of Combine publisher that is a bridge between imperative and reactive programming. Subjects let you manually send events to their subscribers. This is useful for wrapping code that doesn't support Combine into a neat Combine wrapper.
Next, add an initializer to the class:
Notice that you declared a private subject and a public AnyPublisher. Since anyone can add their events to a subject, it's good practice to make it private. The same way you would make setters private and getters public for regular variables. By calling eraseToAnyPublisher, you get a version of the subject that is immutable.
Next, add the following line to the top of login, just before the return statement:
You register as CometChat's user delegate so that it notifies you when a user's status changes.
There's one final bit to add to ChatService. Add the following extension that implements CometChatUserDelegate to the bottom of the file:
Here, you use the subject's send method to send the updated contact through the subject. Anyone listening to the subject (or the erased version) will receive this contact.
Head back to ContactsView.swift to connect it to the code you just wrote. First, add the following method:
This method adds a sink to the publisher you exposed earlier. When an updated contact appears, you find that contact's index and update it. Because items is a state variable, the view will automatically update.
Finally, call this function from the end of body:
Build and run the app and log in as superhero1. All contacts will still be offline because nobody else is using your app yet. You can open another Simulator instance and log in as superhero2.
You'll see the user's online badge turn green. When you exit the app, it will turn grey after a while. Neat!
Okay, that was enough preamble. It's time to get to the bread and butter of your app: Sending and receiving messages. Thankfully, since you are now fetching contacts as well as listening to contact changes, implementing support for messages will be similar. You'll use the same patterns you used for contacts: A request to fetch existing messages and a subject for when new messages arrive in real-time.
First, you'll add a way to convert CometChat's TextMessage into your own Message. Open Message.swift and import CometChat into the file:
This is an optional initializer that tries to create a Message based on CometChat's TextMessage.
You'll start by receiving messages from CometChat. The same way you did for your contacts, add two new properties to track new messages:
Next, erase the subject at the end of init:
You need to set ChatService as CometChat's message delegate. Do so by adding a new line at the top of login:
Next, implement this delegate in an extension at the bottom of the file:
When you get a new message, you'll convert it to your own Message struct and send it through the subject.
Now, open ChatView.swift to add code that listens for new messages. First, add an import to the top of the file:
Then, fetch AppStore from the environment so you get access to the chat service:
Next, add a set of cancellables to the struct:
With those pieces in place, you can create a new function that listens for new messages:
You add a sink to the messages publisher. First, you check that the message is relevant for this view: The message has to either come from the logged-in user, or this screen's receiver. This prevents messages from other users from showing up on this screen. If the message is relevant, you'll add it to the list of messages.
Call this method when the view appears by adding the following line to the bottom of body:
Finally, remove all hard-coded messages from the declaration of messages:
You can run the project now to check everything is working, but you have no way of testing receiving messages: You can't send them yet! Let's fix that.
Now that you can receive messages, it's time to start sending them!
Back in ChatService.swift, add a new function to the class:
The send(message:to:) function will, naturally, send your messages. It receives the message you'll send as well as the user you're sending it to. In the function, you'll construct a new TextMessage with the necessary information. This is a class from CometChat. Besides text messages, CometChat also supports media messages and even custom messages where you can pass any data you like.
Next, add the following code to the function to send the message:
You send the message by calling CometChat's sendTextMessage. If it's sent successfully, you'll add the message to your subject. The view will respond to the message by adding it to the list of messages.
Now you can test this. Run the app and log in as superhero1. When you reach the contacts screen, start chatting with Captain America (superhero2). Launch another simulator instance and, this time, log in as superhero2. Open Iron Man and chat away!
The fact that you're chatting with yourself doesn't make your app any less impressive: You now have a functioning chat app that lets you communicate with another person over a web server! Congrats!
Note: You might notice that, after you get a few messages, new messages appear off-screen. This can be solved by scrolling to the last message whenever a new one appears. In pure SwiftUI, that's easier said than done! Currently, there's no API to manipulate the scroll position of a scroll view. The best way to do this is to wrap UIScrollView in a UIViewControllerRepresentable. This allows SwiftUI to use UIKit's UIScrollView. Check out Apple's official tutorial on how to do this.
Loading past messages
There's only one final thing to add to your app. If you reset the app, all the messages that you sent and received are gone! Don't worry, they're still saved on CometChat — you just need to fetch them.
Back in ChatService.swift, add one final method to the class:
Similarly to how you fetched the contacts, you'll create a message request using a builder. In this case, you'll fetch the last 50 messages sent to and from a specific user.
Next, add the following code to the method:
You create a future like before. When you successfully get the messages, you'll send them through the future by calling promise. If something goes wrong, however, you'll send an error.
Now, open ChatView.swift and add a new method:
You call the function to get the messages and, on success, update the state variable. On failure, you'll print out an error.
Finally, call this method when the view appears at the bottom of body:
Run the app one final time, login in as superhero1 and start chatting with Captain America. You'll see all of your past messages load.
After 7 parts of this SwiftUI course and, really not that much code, you made a fully working chat app!
Throughout this course you've learned:
How to create, compose and layout SwiftUI views.
How to think in a SwiftUI mindset.
How to manage state in SwiftUI.
How to thread data through your app using Combine.
How to use the Environment.
How to initiate network calls and return Futures.
This is more than you need to start going off on your own and discovering SwiftUI for yourself. But, if you want some more in-depth knowledge, here's some reading material:
Apple's WWDC 2019 sessions on SwiftUI are great learning resources in video form. However, some information may be a little outdated now.
SwiftUI by Tutorials, RayWenderlich.com's book on SwiftUI provides some more in-depth knowledge on SwiftUI and might give a different viewpoint than I did.
Additionally, there's also a bundle of SwiftUI, Combine and Catalyst books, so you can learn everything new and shiny in one go.
SwiftUI by Example is a free online book by Paul Hudson, though might be less in-depth than other resources.
SwiftUI is still fairly young so the biggest learning resource you have is your head. Go out, experiment, challenge yourself and figure stuff out! When you do, don't forget to write about it! (Or at least answer StackOverflow questions. :])
Good luck and have fun on your SwiftUI journeys! If you have any questions, feel free to comment below or message me on Twitter.