Skip to content
Lucas Novaes edited this page May 11, 2021 · 14 revisions

Using SwiftPhoenixClient

SwiftPhoenixClient is built to mirror the Javascript Client as closely as possible. If you've used that client, then this should be familiar.

Socket

Creating a Socket

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])

Using a Params Closure to dynamically change Params

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

Socket Event Handlers

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) }

Connecting

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)

Channels

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.

Creating a Channel

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"])

Subscribing to Channel Events

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
}

Joining and Leaving a Channel

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:).

Sending Messages

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") })

Presence

The Presence object provides features for syncing presence information from the server with the client and handling presences joining and leaving.

Creating Presence

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)

Custom Event Options

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)

Syncing State

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()) }

Listing Presences

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)

Handling individual join and leave events

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()) }

Automatic Retain Cycle Handling

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.