The rest of the walkthrough below presumes that you have the application running in your local environment. If you haven't done so yet, follow the instructions in the previous section.
This section of the Swift tutorial explores the code for Animal Forest Chat, a group chat application for iOS using the PubNub Swift SDK. The application connects to a common chat room where users can exchange realtime messages over PubNub's Data Stream Network.
This app uses Protocol Oriented Programming whenever possible. The goal is to keep each component as self-contained as possible, and create new components when one is starting to do too much. This enhances the testability and maintainability of the app, and allows us to reuse a lot of our code.
In addition, the functionality is spread across views, view models, services, and providers.
The separation of responsibilities is as follows:
- Views and controllers handle displaying the UI, and routing to other views
- View models pair with a single view controller to provide the required data and presentation logic
- Services provide specific functionality required by the app
- Providers are typically protocol-wrapped dependencies that services can use to implement their functionality
Creating the chat service
This application displays a single chat view, so we want a service that specializes in providing real-time chat in a specific chat room. The PubNub SDK does the heavy lifting as the chat provider for the service.
PNObjectEventListener objects in the PubNub SDK provide most of the chat functionality for your app. Refer to the PubNub API documentation for additional implementation details.
Start by instantiating the PubNub object.
For simplicity, users and rooms are all stored locally in this tutorial, and you can arbitrarily pick who the sender is. In production, your app will need to determine who the user is before loading the main Chat view controller.
For this app,
PubNub is a default parameter inside our
ChatRoomService. Both these objects are initially created inside the
AppDelegate when the app first launches.
The service initializer takes two parameters: a
ChatRoom and a
ChatProvider. Ignore for the moment that the
PubNub object gets assigned to a different type, and take a look at what
.configure() is doing.
With the configuration created, we pass in the unique ID for the user that will act as the sender, and then return the initialized
Creating a protocol wrapper
Why did we create our
PubNub object, but pass it to our service as a
As described above in Architectural considerations, we want to control the interface of dependencies that we use, especially those that perform networking. The process is broken up into two steps: defining the protocol API, and then implementing that API using an object/value.
Sending a message
To send (publish) a message, we pass the message from the view model to the service. Then, the service uses the provider to make the request. While waiting for a response, we return the
Message value we used, so it can be displayed on the UI.
Here, we define a way to send a message, and then a completion block containing a Result with the response or an error.
Instead of writing a protocol that directly maps to the target object's API,
ChatProvider defines its own API that
PubNub will extend to fit. In addition to the benefits of protocols mentioned earlier, this allows us to modify the generic
PubNub pub/sub API to have a more chat-like feel. We'll look at the different APIs we're defining in the sections below.
This layer should be as thin as possible, and ideally, the content and responses of these methods should act only as a pass-through. We create generic methods to abstract out the complex logic, so we can keep functionality abstracted.
You don't need to create a matching protocol API for every API your dependency offers. Create only what your application requires now; you can always add more in the future.
Listening for changes
The outline for the
PubNub wrapper protocol is complete; next, we need to implement the
PNObjectEventListener to receive real-time events. The
ChatEventProvider class provides an abstraction layer around the
PNObjectEventListener to obfuscate its implementation from the rest of our application:
Notice that this is a
class and not a
protocol, and that it's defined as a singleton. The class is exposed via a property inside our
Then, we implement it inside of our
As we did in Sending a message, we use a thin layer that transforms
PNObjectEventListener events to
Now that it's accessible, we can start listening for changes inside the
ChatRoomService. To start receiving events, we need to first subscribe to a chat room.
Subscribing to the chat room
First, provide a way for the service to subscribe/unsubscribe from the
Then, implement the protocol methods inside the
Notice that we aren't implementing
isSubscribed(on:) inside of the extension. The PubNub SDK already has a method that matches that signature.
ChatService, we can now hook up the new methods.
Fetching occupancy and online members
Along with listening for changes on a channel, we can also look up who is already present when we join. As with publishing, we pass the request from the view model to the service, and the service then passes it to the provider.
Before signaling that the operation is complete, the service adds any users to its presence tracking list.
Fetching messages from history
Retrieving historical messages works in almost the same way as getting a snapshot of the current presence. One small difference is that you pass in the latest message timestamp to avoid fetching duplicate messages.
Connecting the user interface to code
ChatViewModel functionality drives the main chatroom user interface. This view model is commonly made up of a few services that work together to provide a set functionality:
- Real-time chat (
- External network connectivity status (
- Application state tracking (
The view model's role is to provide data to a view controller, so it must provide a way for that view controller to listen for changes in the underlying data. The view controller will
bind() to the view model to indicate that it's ready to start receiving updates. This also signals to the view model that it should
start() its underlying services.
These services provide event listeners to notify the view model when its data changes. The view model then notifies the view controller that the data has changed. Our
ChatRoomService.Listener looks like this:
We've now examined the major functionality of this chat app. Next, you can set up the
ChatViewController to use its view model to populate the UI. This tutorial uses the open-source MessageKit framework, but you can modify your view controller to work with any chat UI.
Testing the app
You can run tests without having to add a network mocking library. Create a
MockChatProvider, and use it anywhere that you previously would have used your PubNub-backed
To start, the
ChatProvider, and then we'll add the required stubs. We're using
send(_ message:) as our example test, and you would follow the same steps to test the rest of the APIs.
This mock implementation doesn't call
PubNub, but instead checks instance properties to see what it should do. If you wanted to simulate an error response, you would assign a value to
publishError, and the mock would return a
.failure result. Likewise, to simulate a successful message, set the
publishResponse and in certain cases the
That's it! All we need to do for the test is set up the mock
messageEvent, configure our service's
listener, and set the response block of the
Set up your own project
Now that you've seen how we built our app, why not get started making your own? You need to create an Xcode project, and install some dependencies.
To create your Xcode project, do the following:
In Xcode, choose Xcode > Settings. On the Source Control tab, verify that your Git settings are correct.
Choose File > New > Project.
Select Single View App on the iOS tab, and click Next.
Name your project, verify that the language is set to Swift, and update the other options as necessary. Then, click Next.
Choose a location on disk for your project, and click Create.
To install the PubNub SDK using the CocoaPods dependency manager, do the following:
Quit Xcode before you do this
If Xcode is running, the following steps won't work correctly.
Follow the instructions at cocoapods.org to install CocoaPods.
If you already have CocoaPods installed, run
pod repo updatein a Terminal window instead.
In a Terminal window, run
pod initin the root directory of your repository.
This creates a Podfile in that directory.
Podfilein an editor, and uncomment the following line:
Then add the following on the next line:
Your Podfile should look similar to the following:
In a Terminal window, run
pod installin your project's root directory to set up CocoaPods' repository structure.
Wait for CocoaPods to finish.
In your Terminal window, run
When Xcode opens your project, you're ready to get started!
Using your PubNub keys
Be careful with keys
Never include your keys in an app, or even in your source-code repository. The following sections show some ways to store them elsewhere, and reference them as needed.
To keep your keys out of your app code and repository, you can store your keys in a
.xcconfig file, and don't commit the file to your source code repository.
Here's how it works: you create a
.xcconfig file without your keys in it, and then use it as a template that you can use to generate
.xcconfig files that contain the keys used by your app.
Refer to Apple's Configuration Settings File format documentation for more information on
For this example, we're using the target name of
To set up a secure mechanism to store and use your keys, do the following:
This key-management approach also works well with CI/CD, and lets you use different keys for different environments. To run this sample project, you can use the script in the project repository.
Create a template file in your project named
The content of your file should be similar to the following (don't replace the placeholder values like
Set the following environment variables inside your terminal configuration (such as
~/.bash_profile) file. These should contain your actual publish and subscribe keys.
In production, you'll need to replace
ANIMALFORESTCHATwith your target's name in all caps.
Write a script to replace the
<SUB_KEY>values with values specific to your project's target, and to populate the keys from a local environment variable.
Replace Value Description Example
Project Target name AnimalForestChat
Project Target name (In all caps) ANIMALFORESTCHAT
Project Configuration name Debug
Your PubNub publish key Alphanumeric string
Your PubNub subscription key Alphanumeric string
pre-installhook at the bottom of your
Podfile, so your key-management script runs every time you run the
You can find an example of this inside this example project's Podfile.
This assumes you're using the script from this example project. If you wrote your own script, swap out everything inside the
system()method to call your script instead.
pod installto generate your
.xcconfigfiles containing your PubNub keys. If you're using the script from this example project, you should find them inside the directory specified by the
Choose File > Add Files and select the non-template files to add these files to your Xcode app Target. Then, set the new files as the active configs for your project.
Add a new entry to your
.gitignorefile, so the new files containing your secret keys won't be added to your source control:
PubNubdictionary entry to your Info.plist defining the
SubKeykey-value pairs. These key names should match the key names inside your generated
Finally, you can reference those values in
Info.plistfrom inside your code.