Swift Quickstart
In this Quickstart, you'll learn how to build your first Swift app that sends and receives messages, and learn how to use status and presence events to enhance your real-time application.
Sample app Quickstart | SDK Getting Started |
---|---|
PubNub Account
Sign in or create an account to create an app on the Admin Portal and get the keys to use in this Quickstart.
Download Sample App
You can clone the repository that contains the files we will use in this Quickstart. Remember to replace the publish and subscribe key placeholders with your own keys.
https://github.com/pubnub/swift-quickstart-platform
Project Setup
To build a Swift project manually:
- Open Xcode and create a new Single View App
- Give it a Product Name,
pnquickstart
- Set the Language to Swift
- Set User Interface to SwiftUI
- Save the project to your file system
- Navigate to File > Swift Packages > Add Package Dependency, then:
- Enter the package repository URL:
https://github.com/pubnub/swift.git
- Click the dialog's Next and Finish buttons, keeping all default options.
- Enter the package repository URL:
Update Project Files
SwiftUI allows you to combine the UI and the app logic in one file while still providing a separation of concerns (SoC) using Stores and Models. You'll have to make a single line of code change to one file and a complete replacement of all the code in another file.
Open the
SceneDelegate.swift
file and replace:let contentView = ContentView()
with
let contentView = ContentView(pubnubStore: PubNubStore())
This code opens the desired view,
ContentView
, which contains the UI code, and initializes thePubNubStore
, anObservableObject
, that contains all the app logic that binds the UI with the data.Open the
ContentView.swift
file. Overwrite all existing code with the following code and save your changes, to create the simple UI for the app.
The objective here is to keep things as simple as possible so that the UI code doesn't hinder the process of learning how to use PubNub, while also following the best practices of the Swift language as much as possible.import SwiftUI import PubNub struct ContentView: View { @ObservedObject var pubnubStore: PubNubStore @State var entry = "Mostly Harmless." var body: some View { VStack { Spacer() TextField("", text: $entry, onCommit: submitUpdate) .textFieldStyle(RoundedBorderTextFieldStyle()) .frame(width: 300.0, height: 40) Spacer() Button(action: submitUpdate) { Text("SUBMIT UPDATE TO THE GUIDE") .padding() .foregroundColor(Color.white) .background(entry.isEmpty ? Color.secondary : Color.red) .cornerRadius(40) } .disabled(entry.isEmpty) .frame(width: 300.0) Spacer() List { ForEach(pubnubStore.messages.reversed()) { message in VStack(alignment: .leading) { Text(message.messageType) Text(message.messageText) } } } Spacer() } } func submitUpdate() { if !self.entry.isEmpty { pubnubStore.publish(update: EntryUpdate(update: self.entry)) self.entry = "" } // Hides keyboard UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } // MARK:- View Stores class PubNubStore: ObservableObject { @Published var messages: [Message] = [] var pubnub: PubNub let channel: String = "the_guide" let clientUUID: String = "ReplaceWithYourClientIdentifier" init() { var pnconfig = PubNubConfiguration(publishKey: "myPublishKey", subscribeKey: "mySubscribeKey") pnconfig.uuid = clientUUID self.pubnub = PubNub(configuration: pnconfig) startListening() subscribe(to: self.channel) } lazy var listener: SubscriptionListener? = { let listener = SubscriptionListener() listener.didReceiveMessage = { [weak self] event in if let entry = try? event.payload.codableValue.decode(EntryUpdate.self) { self?.display( Message(messageType: "[MESSAGE: received]", messageText: "entry: \(entry.entry), update: \(entry.update)") ) } } listener.didReceivePresence = { [weak self] event in let userChannelDescription = "event uuid: \(event.metadata?.codableValue["pn_uuid"] ?? "null"), channel: \(event.channel)" self?.display( Message(messageType: "[PRESENCE: \(event.metadata?.codableValue["pn_action"] ?? "null")]", messageText: userChannelDescription) ) } listener.didReceiveSubscriptionChange = { [weak self] event in switch event { case .subscribed(let channels, _): self?.display(Message(messageType: "[SUBSCRIPTION CHANGED: new channels]", messageText: "channels added: \(channels[0].id)")) self?.publish(update: EntryUpdate(update: "Harmless.")) default: break } } listener.didReceiveStatus = { [weak self] event in switch event { case .success(let connection): self?.display(Message(messageType: "[STATUS: connection]", messageText: "state: \(connection)")) case .failure(let error): print("Status Error: \(error.localizedDescription)") } } return listener }() func startListening() { if let listener = listener { pubnub.add(listener) } } func subscribe(to channel: String) { pubnub.subscribe(to: [channel], withPresence: true) } func display(_ message: Message) { self.messages.append(message) } func publish(update entryUpdate: EntryUpdate) { pubnub.publish(channel: self.channel, message: entryUpdate) { [weak self] result in switch result { case let .success(timetoken): self?.display( Message(messageType: "[PUBLISH: sent]", messageText: "timetoken: \(timetoken.formattedDescription) (\(timetoken.description))") ) case let .failure(error): print("failed: \(error.localizedDescription)") } } } } // MARK:- Models struct EntryUpdate: JSONCodable { var update: String var entry: String init(update: String, entry: String = "Earth") { self.update = update self.entry = entry } } struct Message: Identifiable { var id = UUID() var messageType: String var messageText: String } // MARK:- Extension Helpers extension DateFormatter { static let defaultTimetoken: DateFormatter = { var formatter = DateFormatter() formatter.timeStyle = .medium formatter.dateStyle = .short formatter.locale = Locale(identifier: "en_US_POSIX") return formatter }() } extension Timetoken { var formattedDescription: String { return DateFormatter.defaultTimetoken.string(from: timetokenDate) } } // MARK:- View Preview struct ContentView_Previews: PreviewProvider { static let store = PubNubStore() static var previews: some View { ContentView(pubnubStore: store) } }
Replace Pub/Sub Key Placeholders
Remember to replace the myPublishKey and mySubscribeKey placeholders with your own PubNub publish and subscribe keys.
Run the App
If this is your first Swift project, it will take a bit longer to start the simulator. Subsequent runs will load almost immediately.
1. Click the Run button. You'll see the iPhone simulator that displays a screen similar to this: The UI is rather simple but uses a list view that cleanly displays each event. A message was published on app start up and some other events were displayed. 2. Submit a new entry A new entry update has been auto populated for you: "Mostly Harmless.". You can change the text or just hit the SUBMIT UPDATE TO THE GUIDE button to publish this new update. You'll see the new update inserted at the top of the current messages while the older messages scroll down. You'll also see that now the entry update field is cleared for you to enter something new. | ![]() |
Message Display Order
You may see the [MESSAGE: received] appear before the [PUBLISH: sent] message or possibly after, since all this code is executed asynchronously.
Walkthrough
Let's review exactly what's happening for all this to work properly.
Include PubNub SDK
You included the PubNub Swift SDK using the Swift Package Manager. You could also use Cocoapods, Carthage or other options which are all documented on the Swift SDK docs page.
Create the UI
The UI code is mixed with the app logic code. The var body:
portion of the ContentView
code is the UI configuration code with precise data binding integration.
Code Overview
Here's an overview of what's happening in the ContentView.swift
file:
SceneDelegate
is called by default by the app. This initializes theContentView
and thePubNubStore
(ObservableObject
).- The
ContentView
contains abody
(var body: some View { // lots of UI components here }
) that contains the UI elements. This is loaded and displayed. PubNubStore.init
- TheSceneDelegate
also initializes thePubNubStore
which contains the following logic:- init the PubNub object
- add the listener for the message and event handlers
- subscribe to a channel
displayMessages
- this is where you update the messages UI when new messages or events are received.submitUpdate
- this is a SwiftUI Button action (button click) handler to which you add code to submit new entry updates. The confusing part here is thatpubnubStore.publish
isn't a PubNubpublish
; it's thepublish
function forPubNubStore ObservableObject
, and you're calling the PubNubpublish
function from the ObservableObject'spublish
function.
Initialize the PubNub Object
The PubNub object allows you to make PubNub API calls, like publish
and subscribe
, among others. There are more configuration properties than what you see here, but this is all you need for this Quickstart. Of course, you would use your own pub/sub keys below. It's also important to set the uuid
for the client. This is typically received from your server after the client logs in successfully.
Successive App Run Behavior
If you test run the app multiple times within a few minutes, you may not see the presence join event appear because the client's UUID is the same as the previous run and is still considered active from the previous run.
var pnconfig = PubNubConfiguration(publishKey: "myPublishKey", subscribeKey: "mySubscribeKey")
pnconfig.uuid = clientUUID
Proper PubNub Initialization
If your app has several views that require the PubNub object, then it can be initialized in the AppDelegate
and passed from one view to another using segues. However, if PubNub is only required in seldom used sections of your app, you might just initialize it on demand in that view. You may also implement other common strategies for building your UIs and passing data.
Listen for messages & events
The listener
object is declared as a data member of the ContentView
struct.
// Create a new listener instance
let listener = SubscriptionListener()
Next, we add the event callbacks, and add the listener
.
// Add listener event callbacks
listener.didReceiveMessage = { [weak self] event in
if let entry = try? event.payload.codableValue.decode(EntryUpdate.self) {
self?.display(
Message(messageType: "[MESSAGE: received]", messageText: "entry: \(entry.entry), update: \(entry.update)")
)
}
}
listener.didReceivePresence = { [weak self] event in
let userChannelDescription = "event uuid: \(event.metadata?.codableValue["pn_uuid"] ?? "null"), channel: \(event.channel)"
self?.display(
Message(messageType: "[PRESENCE: \(event.metadata?.codableValue["pn_action"] ?? "null")]", messageText: userChannelDescription)
)
}
listener.didReceiveSubscriptionChange = { [weak self] event in
switch event {
case .subscribed(let channels, _):
self?.display(Message(messageType: "[SUBSCRIPTION CHANGED: new channels]", messageText: "channels added: \(channels[0].id)"))
self?.publish(update: EntryUpdate(update: "Harmless."))
default: break
}
}
listener.didReceiveStatus = { [weak self] event in
switch event {
case .success(let connection):
self?.display(Message(messageType: "[STATUS: connection]", messageText: "state: \(connection)"))
case .failure(let error):
print("Status Error: \(error.localizedDescription)")
}
}
return listener
}()
// Start receiving subscription events
func startListening() {
if let listener = listener {
pubnub.add(listener)
}
}
In this app, you only need didReceiveMessage
, didReceivePresence
, didReceiveSubscriptionChange
, and didReceiveStatus
handlers. There are several other handlers that aren't included in this Quickstart; you'll use these for other features of PubNub.
Receiving Messages
The didReceiveMessage
handler receives all messages published to all channels subscribed by this client. When a message is received, you call the displayMessage
function, which renders it in the UI.
didReceiveMessage handler
listener.didReceiveMessage = { [weak self] event in
if let entry = try? event.payload.codableValue.decode(EntryUpdate.self) {
self?.display(
Message(messageType: "[MESSAGE: received]", messageText: "entry: \(entry.entry), update: \(entry.update)")
)
}
}
Receiving Status Events
The didReceiveStatus
handler allows you to monitor and manage the connection to the PubNub Platform. When the client subscribes to a channel, a connecting
status event will be received. When the connection is successful, a connected
status event is received to confirm. You're using these events to monitor and log the occurrence of these events only.
listener.didReceiveStatus = { [weak self] event in
switch event {
case .success(let connection):
self?.display(Message(messageType: "[STATUS: connection]", messageText: "state: \(connection)"))
case .failure(let error):
print("Status Error: \(error.localizedDescription)")
}
}
Receiving Subscription Events
The didReceiveSubscriptionChange
handler receives events when channels are added to (subscribed) or removed from (unsubscribed) the current list of channels. When the client subscribes to a channel, a subscribed
event is received to indicate a successful channel subscription. You're using this event to submit an initial entry update (publishing a message to a channel).
listener.didReceiveSubscriptionChange = { [weak self] event in
switch event {
case .subscribed(let channels, _):
self?.display(Message(messageType: "[SUBSCRIPTION CHANGED: new channels]", messageText: "channels added: \(channels[0].id)"))
self?.publish(update: EntryUpdate(update: "Harmless."))
default: break
}
}
Receiving Presence Events
The didReceivePresence
handler allows you to monitor when this and other clients join and leave channels. The subscribe
call includes a withPresence
parameter that indicates that the client wishes to receive presence events on the subscribed channels. In this app, you'll only receive a join
event. If you were to execute the app using another client (like the PubNub Dev Console), you would see a join
event for that client, too. And you would see leave
events when the other clients unsubscribe from a channel or a timeout
event if they go offline without first unsubscribing.
listener.didReceivePresence = { [weak self] event in
let userChannelDescription = "event uuid: \(event.metadata?.codableValue["pn_uuid"] ?? "null"), channel: \(event.channel)"
self?.display(
Message(messageType: "[PRESENCE: \(event.metadata?.codableValue["pn_action"] ?? "null")]", messageText: userChannelDescription)
)
}
Subscribe to a Channel
This subscribe
opens a connection to PubNub and waits for messages to be publish
-ed to this channel. You can subscribe to more than one channel at a time but you only need the one for this Quickstart. The withPresence:true
parameter enables presence events to be sent for the channel being subscribed to in this call. The PubNub subscribe
is encapsulated in a custom subscribe
function that can be called from anywhere by passing in a channel name to subscribe to.
func subscribe(to channel: String) {
pubnub.subscribe(to: [channel], withPresence: true)
}
Publishing Messages
The submitUpdate
function is invoked when the SUBMIT UPDATE TO THE GUIDE button is clicked. This is a SwiftUI Button action (button click) handler to which you add code to submit new entry updates.
Button(action: submitUpdate) {
Text("SUBMIT UPDATE TO THE GUIDE")
.padding()
.foregroundColor(Color.white)
.background(entry.isEmpty ? Color.secondary : Color.red)
.cornerRadius(40)
}
func submitUpdate() {
if !self.entry.isEmpty {
pubnubStore.publish(update: EntryUpdate(update: self.entry))
self.entry = ""
}
// Hides keyboard
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
It's also worth pointing out that submitUpdate
is part of the ContentView
struct
. This function wraps a publish
call to the PubNubStore
class (pubnubStore.publish
) and not PubNub's publish
API. It's in the PubNubStore
's publish
function that you're calling the PubNub publish
API.
The messages that this client publishes to the channel will be received by this and other clients subscribed to that channel, in the didReceiveMessage
handler. The success of a publish
is updated in the messages list element with the publish timetoken
value and a formatted datetime value.
Displaying Messages
The displayMessage
function is a single place where all updates to the messages UI component are handled in a standard way. Anytime a new message or event needs to be displayed, a Message
struct item is added to the pubnubStore.messages
array which is bound to the List
element automating the update of the UI. The list element iterates over the messages
array in reverse order. This results in the latest item always appearing at the top of the list.
func display(_ message: Message) {
self.messages.append(message)
}
Message
struct (Model) bound to the List
element in the UI
struct Message: Identifiable {
var id = UUID()
var messageType: String
var messageText: String
}
List
UI binding to the messages @State var (an array of Message
struct)
List {
ForEach(pubnubStore.messages.reversed()) { message in
VStack(alignment: .leading) {
Text(message.messageType)
Text(message.messageText)
}
}
}
Note for Advanced Users
This Quickstart app was created in a manner that avoids as much complexity as possible while still guiding you in the direction of best practices of using PubNub.
Memory Management
Swift provides the ability to more precisely inform the garbage collection process how to manage the objects that you create. The use of weak self
in the Quickstart's event handler code may cause some developers to wonder why this is necessary and how one would know to do that instead of some other declaration or even none. This comes with further study and experience using the Swift language. Refer to Apple's Language Guide on ARC (Automatic Reference Counting).
Use `weak` reference
As quoted from Apple docs, use a weak self
reference whenever it's valid for that reference to become nil at some point during its lifetime. Search weak self
to learn more.
Struct Models
A struct
model (struct Message: Identifiable
) is used to define the messages that are listed in the list view. A list view requires that the struct
model conform to the Identifiable
protocol. This protocol requires that you have one property of the struct named id
. In this Quickstart app, UUID()
is used as that id. You can use anything that will uniquely identify the item in the list of items on that device (they need not be universally unique).
The Apple Swift docs don't provide much more on the Identifiable
protocol so you're encouraged to search for third-party tutorials that will guide you further with best practices around using struct
s as models in your Swift apps.
Observable Objects
An observable object (class PubNubStore: ObservableObject
) is a custom object for your data that can be bound to a view from storage in SwiftUI’s environment. SwiftUI watches for any changes to observable objects that could affect a view, and displays the correct version of the view after a change.
Lazy Variables
The point of lazy properties is that they're computed only when they're first needed, after which their value is saved. This provides faster initial loading of the ContentView
in this Quickstart.
Extensions
Extensions add new functionality to an existing class, structure, enumeration, or protocol type. The extension
s included in this Quickstart work together to provide a human readable date-time format of the PubNub 17 digit timetoken. If you would like to learn more, review Apple's Language Guide on Extensions.
Next Steps
Now that you have your Swift app up and running with PubNub, learn more about what else you can do with Channels. To discover what else our Swift SDK offers, go to our Swift SDK Reference.