-
Notifications
You must be signed in to change notification settings - Fork 147
Usage Guide
SwiftPhoenixClient is built to mirror the Javascript Client as closely as possible. If you've used that client, then this should be familiar.
First, you need to create the socket that will join channels, receive events, and send messages
let socket = Socket("ws://localhost:4000/socket/websocket")
You can pass optional parameters that will be sent to the server when connecting the socket. This will create a Socket that is pointed to "ws://localhost:4000/socket/websocket"
with query parameters token
and user
appended to the URL.
let socket = Socket("ws://localhost:4000/socket/websocket", params: ["token": "abc123", "user": 15])
Sometimes, you need to change the params used by the Socket throughout the lifecycle of your application. For example, you've connected the Socket successfully but then the Socket's authentication credentials expire and the Socket disconnects. It will begin to attempt to reconnect, but it will be using the expired credentials.
You can use a ParamsClosure
in this situation.
var authToken = "abc123"
let socket = Socket("ws://localhost:4000/socket/websocket", paramsClosure: ["token": authToken])
socket.connect() // ?token=abc123
// authToken changes.
authToken = "xyz987"
// Socket reconnects for some reason (manually or network gets dropped)
socket.disconnect()
socket.connect() // ?token=xyz987
Once you've created your socket, you can add event hooks to be informed of socket open, closed, error, etc.
socket.onOpen { print("Socket Opened") }
socket.onClose { print("Socket Closed") }
socket.onError { (error) in print("Socket Error", error) }
// For Logging
socket.logger = { message in print("LOG:", message) }
That's all the setup there is to do. All that's left is to connect to your socket. When you're done, be sure to disconnect!
socket.connect() // Opens connection to the server
socket.disconnect() // Closes connection to the server
socket.releaseCallbacks() // Releases any event callback hooks (onOpen, onClose, etc)
Once your socket is created, you can join
channel topics which listen for specific events and allow for sending data do a topic. Whenever sending data, you can use the .receive()
hook to get the status of the outbound push message.
Channels cannot be created directly but instead are created through the socket
object by calling socket.channel(topic:, params:)
with the topic
of the channel. You can optionally pass parameters to be sent when joining the channel
let socket = Socket("ws://localhost:4000/socket/websocket")
let channelA = socket.channel("room:a")
let channelB = socket.channel("room:b", params: ["token": "Room Token"])
Once you have a channel created, you can specify any number of events
you would like your Channel to subscribe to by calling channel.on(event:, callback:)
.
let socket = Socket("ws://localhost:4000/socket/websocket")
let channel = socket.channel("room:123", params: ["token": "Room Token"])
// Listen for `new_msg` events
channel.on("new_msg") { [weak self] (message) in
let payload = message.payload
let content = payload["content"] as? String
let username = payload["username"] as? String
print("\(username) sent the message: \(content)")
}
// Also listen for `new_user` events
channel.on("new_user") { [weak self] (message) in
let username = message.payload["username"] as? String
print("\(username) has joined the room!")
}
NOTE: It is recommended to include weak self
in the closures capture list to prevent memory leaks. An alternative is you can use the automatic retain cycle handling provided by the client. See Automatic Retain Cycle Handling at the end.
There are also special events that you can listen to on a Channel.
/// Called when errors occur on the channel
channel.onError { (payload) in print("Error: ", payload) }
/// Called when the channel is closed
channel.onClose { (payload) in print("Channel Closed") }
/// Called before the `.on(event:, callback:)` hook is called for all events. Allows you to
/// hook in an manipulate or spy on the incoming payload before it is sent out to it's
/// bound handler. You must return the payload, modified or unmodified
channel.onMessage { (event, payload, ref) -> Payload in
var modifiedPayload = payload
modifiedPayload["modified"] = true
return modifiedPayload
}
Now that all of your channel has been created and setup, you will need to join before any events will be received.
// Join the Channel
channel.join()
.receive("ok") { message in print("Channel Joined", message.payload) }
.receive("error") { message in print("Failed to join", message.payload) }
Once you are done with a Channel, be sure to leave it. This will close the Channel on the server and clean up active Channels in your socket
.
channel.leave()
PLEASE NOTE, EACH CHANNEL INSTANCE CAN ONLY BE JOINED ONCE. If you would like to rejoin a channel after you have left it, you will need to create a new instance using socket.channel(topic:)
.
Once you have a channel, you can begin to push messages through it
channel
.push("new:msg", payload: ["body": "message body"])
.receive("ok", handler: { (payload) in print("Message Sent") })
The Presence object provides features for syncing presence information from the server with the client and handling presences joining and leaving.
To sync presence state from the server, first instantiate an object and pass your channel in to track lifecycle events:
let channel = socket.channel("some:topic")
let presence = Presence(channel)
If you have custom syncing state events, you can configure the Presence
object to use those instead.
let options = Options(events: [.state: "my_state", .diff: "my_diff"])
let presence = Presence(channel, opts: options)
Next, use the presence.onSync
callback to react to state changes from the
server. For example, to render the list of users every time the list
changes, you could write:
presence.onSync { renderUsers(presence.list()) }
presence.list(by:)
is used to return a list of presence information based on the
local state of metadata. By default, all presence metadata is returned, but
a listBy function can be supplied to allow the client to select which
metadata to use for a given presence. For example, you may have a user
online from different devices with a metadata status of "online", but they
have set themselves to "away" on another device. In this case, the app may
choose to use the "away" status for what appears on the UI. The example
below defines a listBy function which prioritizes the first metadata which
was registered for each user. This could be the first tab they opened, or
the first device they came online from:
let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in
let first = pres["metas"]!.first!
first["count"] = pres["metas"]!.count
first["id"] = id
return first
}
let onlineUsers = presence.list(by: listBy)
(NOTE: The underlying behavior is a map
on the presence.state
. You are
mapping the state
dictionary into whatever data structure suites your needs)
The presence.onJoin and presence.onLeave callbacks can be used to react to individual presences joining and leaving the app. For example:
let presence = Presence(channel)
presence.onJoin { [weak self] (key, current, newPres) in
if let cur = current {
print("user additional presence", cur)
} else {
print("user entered for the first time", newPres)
}
}
presence.onLeave { [weak self] (key, current, leftPres) in
if current["metas"]?.isEmpty == true {
print("user has left from all devices", leftPres)
} else {
print("user left from a device", current)
}
}
presence.onSync { renderUsers(presence.list()) }
The client comes with an optional API that will automatically manage retain cycles for you in your event hooks. It can be used by simply calling the delegate*(to:
) sibling method of any of the other event hook methods. This works by taking in a reference to an owner
and then passing that owner
back to the event hook to be referenced. Then, when the owner gets dereferenced, the callback hook goes with it.
This simply provides a cleaner API to developers, removing the need for [weak self]
capture list in callbacks. These APIs are used internally by the client as well to prevent memory leaks between classes.
// Example for socket onOpen
socket.onOpen { [weak self] self?.addMessage("Socket Opened") }
socket.delegateOnOpen(to: self) { (self) in self.addMessage("Socket Opened") }
// Example for channel events
channel.on("new_user") { [weak self] (message) in self?.handle(message) }
channel.delegateOn("new:msg", to: self) { (self, message) in self.handle(message) }
// Example for receive hooks
channel.join().receive("ok") { [weak self] self?.onJoined() }
channel.join().delegateReceive("ok", to: self, callback: { (self, message) in self.onJoined() }
You can lean more about this approach here. The API is optional and does not need to be used if you prefer not to.