Swift
Sariska provides a Swift API and easy-to-integrate SDKs for building real-time features into your iOS, macOS, watchOS, and tvOS applications. Easily add in-app chats, instant messaging, and other real-time functionalities to your apps.
Key Features:
Real-time messaging for in-app chats and instant messaging
Easy installation using Cocoa Pods or Carthage
Socket creation and management
Channel creation, joining, and leaving
Sending messages, poll votes, and message replies
Presence management (track: typing, joining and leaving users)
History management (fetching chat history and specific messages)
Installation
Step 1 : Install the Phoenix Client
Using Cocoa Pods
To install SwiftPhoenixClient using CocoaPods, add the following line to your Podfile:
pod "SwiftPhoenixClient", '~> 2.0'
If you are using RxSwift extensions, add the following line to your Podfile:
pod "SwiftPhoenixClient/RxSwift", '~> 2.0'
If your version requirements require iOS 13 and below, add the following line to your Podfile:
pod "SwiftPhoenixClient/Starscream", '~> 2.0'
Using Carthage
To install SwiftPhoenixClient using Carthage, add the following line to your Cartfile:
github "davidstump/SwiftPhoenixClient" ~> 2.0
Step 2 : Create Socket
To establish a connection with Sariska's real-time messaging platform, you'll need to create a socket instance. This socket will handle joining channels, receiving events, and sending messages.
let token = {your-token}
let socket = Socket("wss://api.sariska.io/api/v1/messaging/websocket", { token: token })
/// Handles socket opening event
socket.onOpen { print("Socket Opened") }
/// Handles socket closing event
socket.onClose { print("Socket Closed") }
/// Handles socket error events
socket.onError { (error) in print("Socket Error", error) }
/// Enables logging for socket events
socket.logger = { message in print("LOG:", message) }
/// Opens connection to the server
socket.connect()
Disconnect Socket
Close the WebSocket connection to the Sariska server. This will terminate all active channels and prevent further communication with the server.
/// Close connection to the server
socket.disconnect()
/// Removes all event callback hooks, such as 'onOpen' and onClose
socket.releaseCallback()
Step 3 : Create Channel
Channels cannot be created directly; instead, they are created through the socket object by calling socket.channel(topic)
with the topic of the channel. The topic is a string that identifies the channel, and it can be any string you like.
Channel Prefix
Each channel name starts with a prefix that determines the message serialization format and persistence behavior.
chat:
Use this prefix for persisting messages to the database. This prefix requires a fixed schema and is ideal for chat applications.
let channel = socket.channel("chat:room123");
rtc:
Use this prefix for scenarios where message persistence is not necessary. This prefix allows sending arbitrary JSON data, making it suitable for events in multiplayer games, IoT applications, and others.
let channel = socket.channel("rtc:room123");
sariska:
Use this prefix for performance-critical applications that utilize Flatbuffers as the serialization format and do not require message persistence. This prefix provides zero-copy serialization/deserialization, significantly enhancing performance.
let channel = socket.channel("sariska:room123");
Handle Errors
When an error occurs on the channel, the onError
callback is triggered. The callback receives the error information in payload, if available.
channel.onError { (payload) in print("Error: ", payload) }
Close Channel
When the channel is closed, the onClose
callback is invoked. This signifies that the communication on the channel has ended and no further data can be exchanged.
channel.onClose { (payload) in print("Channel Closed") }
Step 4 : Join and Leave Channel
To join a channel, call the join()
method on the channel object. The join()
method returns a promise that resolves when the client has successfully joined the channel. When sending data, you can utilize the .receive()
hook to monitor the status of the outgoing push message.
/// Join the Channel
channel.join()
.receive("ok") { message in print("Channel Joined", message.payload) }
.receive("error") { message in print("Failed to join", message.payload) }
/// Leave the Channel
channel.leave()
Channel User Joined
/// Subscribe to the "new_user" event, when new user join the room
channel.on("new_user") { [weak self] (message) in
/// Extract the username from the message payload
let username = message.payload["username"] as? String
/// Print a message indicating that the new user has joined the room
print("\(username) has joined the room!")
}
Channel New Message
channel.on("new_msg") { [weak self] (message) in
/// Extract the payload from the message
let payload = message.payload
/// Extract the content from the payload
let content = payload["content"] as? String
/// Extract the username from the payload
let username = payload["username"] as? String
/// Print the message to the console
print("\(username) sent the message: \(content)")
}
Send Message
Once you've established a connection to a channel, you can start sending messages to other connected clients. To send a message, use the push()
method on the channel object.
channel
/// Send a message with payload
.push("new_messgage", payload: ["body": "message body"])
/// Receive a response message
.receive("ok", handler: { (payload) in
print("Message Sent")
})
Send Message Reply
channel
/// Send a message with payload
.push("new_messgage_reply", payload: ["body": "message body"])
/// Receive a response message
.receive("ok", handler: { (payload) in
print("Message Sent")
})
Channel Poll Vote
let pollOptions = ["Option 1", "Option 2", "Option 3"]
/// Build the payload with the poll question, content type, and options
let payload: [String: Any] = [
"content": "poll question?",
"content_type": "poll",
"options": pollOptions
]
/// Send the new_vote event with the payload
channel
.push("new_vote", payload: payload)
.receive("ok", handler: { (payload) in
print("Poll Vote Sent")
})
Attach Media Files to Chat Messages
Attaching media files to chat messages involves obtaining a presigned URL, uploading the file to the presigned URL, and then sending the file information to the chat server.
Obtain a Presigned URL
To obtain a presigned URL, make a POST request to the API endpoint. The request payload should be empty, and the Authorization
header should contain your bearer token. The bearer token authenticates the request and returns a URL that can be used to upload content.
import Foundation
func getPresignedURL() async throws -> URL {
let url = URL(string: "https://api.sariska.io/api/v1/misc/get-presigned")!
var request = URLRequest(url: url)
/// Set the HTTP method to `POST`
request.httpMethod = "POST"
/// Add bearer token to the Authorization header
request.setValue("Bearer your-token", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(from: request)
let responseBody = try JSONDecoder().decode(ResponseBody.self, from: data)
return URL(string: responseBody.presignedUrl)!
}
Upload the File
After obtaining the presigned URL, the file can be uploaded to the URL using the PUT method.
func uploadFile(fileURL: URL) async throws {
/// Retrieve the pre-signed URL for uploading the file
let presignedURL = try await getPresignedURL()
/// Read the contents of the file into a `Data` object
let fileData = try Data(contentsOf: fileURL)
/// Create a `URLRequest` object with the pre-signed URL
var request = URLRequest(url: presignedURL)
/// Set the HTTP method to `PUT`
request.httpMethod = "PUT"
/// Set the ACL header to "public-read" to make the file publicly accessible
request.setValue("public-read", forHTTPHeaderField: "ACL")
/// Set the Content-Disposition header to "attachment" to indicate that the response is a file attachment
request.setValue("attachment", forHTTPHeaderField: "Content-Disposition")
/// Upload the file data using the `URLSession` object
try await URLSession.shared.upload(for: request, from: fileData)
}
/// Codable struct to represent the response body
struct ResponseBody: Codable {
let presignedUrl: String
}
let fileURL = /// URL to the file you want to upload
do {
try await uploadFile(fileURL: fileURL)
print("File uploaded successfully.")
} catch {
print("Error uploading file: \(error)")
}
Chat History
Retrieve the chat history using two methods:
By Subscribing to Events
Subscribe to the
archived_message
event to receive the last 1000 archived messages in reverse chronological order.
channel.on("archived_message") { (payload) in
if let messages = payload["messages"] as? [String: Any] {
/// Print the received messages
print("Received archived messages: \(messages)")
}
}
Subscribe to the
archived_message_count
event to get the total number of messages in the chat history.
channel.on("archived_message_count") { (payload) in
if let page = payload["page"] as? [String: Any],
let count = page["count"] as? Int {
/// Print the received message count
print("Total message count: \(count)")
}
}
To retrieve a list of messages in the chat history, trigger the
archived_message
event to obtain the messages. Specify thesize
parameter to determine the number of messages you wish to fetch, and set theoffset
parameter as the starting index of the messages andgroup_by_day
to group messages by day.
channel.push("archived_message", payload: ["page": ["offset": 20, "size": 20, "group_by_day": false ]])
To receive the total count of messages at any given time, initiate the archived_message_count trigger and subscribe to the corresponding event by listening for
archived_message_count
.
channel.push("archived_message_count")
Using the Messages API
Make a GET request to the API endpoint to fetch the chat history for a specific room.
func fetchChatHistory(completion: @escaping (Result<[String: Any], Error>) -> Void) {
/// Construct the request URL
let url = URL(string: "https://api.sariska.io/api/v1/messaging/rooms/{room_name}/messages")!
/// Modify the request to use the GET method
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("Bearer your-token", forHTTPHeaderField: "Authorization")
/// Send the GET request to the API endpoint
URLSession.shared.dataTask(with: request) { (data, response, error) in
/// Check for errors
if let error = error {
completion(.failure(error))
return
}
/// Check for successful response
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
let errorMessage = "Failed with status code \(statusCode)"
let error = NSError(domain: "APIError", code: statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage])
completion(.failure(error))
return
}
/// Parse the response data
do {
if let data = data {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
completion(.success(json ?? [:]))
} else {
completion(.success([:]))
}
} catch {
completion(.failure(error))
}
}.resume()
}
fetchChatHistory { result in
switch result {
case .success(let chatHistory):
print("Chat history: \(chatHistory)")
case .failure(let error):
print("Error fetching chat history: \(error)")
}
}
Fetch Specific Message
Retrieve any specific message from a room. It takes the room ID
and message ID
as parameters and sends a GET request to the Sariska API to fetch the specified message.
func fetchSpecificMessage(messageID: String, completion: @escaping (Result<[String: Any], Error>) -> Void) {
/// Construct the request URL
let urlString = "https://api.sariska.io/api/v1/messaging/rooms/{room_name}/messages/\(messageID)"
guard let url = URL(string: urlString) else {
completion(.failure(NSError(domain: "InvalidURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
return
}
/// Construct the request
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("Bearer your-token", forHTTPHeaderField: "Authorization")
/// Send the GET request to the API endpoint
URLSession.shared.dataTask(with: request) { (data, response, error) in
// Check for errors
if let error = error {
completion(.failure(error))
return
}
/// Check for successful response
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
let errorMessage = "Failed with status code \(statusCode)"
let error = NSError(domain: "APIError", code: statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage])
completion(.failure(error))
return
}
/// Parse the response data
do {
if let data = data {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
completion(.success(json ?? [:]))
} else {
completion(.success([:]))
}
} catch {
completion(.failure(error))
}
}.resume()
}
let messageId = "your-message-id"
fetchSpecificMessage(messageID: messageId) { result in
switch result {
case .success(let specificMessage):
print("Specific message: \(specificMessage)")
case .failure(let error):
print("Error fetching specific message: \(error)")
}
}
Presence
The Presence object facilitates real-time synchronization of presence information between the server and the client, enabling the detection of user join and leave events.
Create a Presence Instance
To establish presence synchronization, instantiate a Presence object and provide the channel to track presence lifecycle events:
let channel = socket.channel("some:topic")
let presence = Presence(channel)
Custom Event Options
By default, the Presence
object uses built-in events for syncing state changes. However, you can configure the Presence object to use custom events instead. This allows you to better integrate with your existing application logic.
let options = Options(events: [.state: "my_state", .diff: "my_diff"])
let presence = Presence(channel, opts: options)
State Synchronization
The presence.onSync
callback triggers whenever the server updates the presence list. This allows you to update your UI based on the latest presence information, such as dynamically generating a user list.
presence.onSync { renderUsers(presence.list()) }
Handle Individual Join and Leave Events
To handle individual join and leave events, use the presence.onJoin
and presence.onLeave
callbacks. These provide information about the user and their presence state. Here is an instance:
let presence = Presence(channel)
/// Detect if the user has joined for the first time or from another tab/device
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)
}
}
/// Detect if the user has left all tabs/devices, or is still present
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)
}
}
/// Receive presence data from server
presence.onSync { renderUsers(presence.list()) }
Retrieve Presence Information
The presence.list(by:)
method retrieves a list of presence information based on the local metadata state. By default, it returns the entire presence metadata. Alternatively, a listBy
function can be provided to filter the metadata for each presence.
For instance, if a user is online from multiple devices, each with a metadata status of "online," but their status is set to "away" on another device, the application might prefer to display the "away" status in the UI.
The following example defines a listBy
function that prioritizes the first metadata registered for each user, representing the first tab or device they used to connect:
let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in
let first = pres["metas"]!.first!
first["count"] = pres["metas"]!.count
/// Set the user ID for the presence metadata
first["id"] = id
/// Return the prioritized presence metadata
return first
}
/// Retrieve a list of online users based on the prioritized presence metadata
let onlineUsers = presence.list(by: listBy)
User Typing
Send information about a user who is typing. This information can be used to update the chat interface, such as displaying a "user is typing" indicator next to the user's name.
Send User Typing Event
When a user starts typing, the following code sends a typing event to other peers.
channel.push("user_typing", payload: ["typing": true ])
Receive User Typing Event
Other peers in the chat can listen for the typing event on the same channel.
channel.on("user_typing") { [weak self] (typing_response) in
print("\(typing_response)!")
}
Handle Automatic Retain Cycle
The Sariska client library offers an optional API to automatically manage retain cycles for event hooks, simplifying your code and preventing memory leaks.
How it works:
Each event hook has a corresponding
delegate*(to:)
method.This method takes a reference to an owner object and passes it back to the event hook.
When the owner object is deinitialized, the associated event hook is automatically dereferenced, preventing retain cycles.
Benefits:
Cleaner API: Eliminates the need for
[weak self]
capture lists, making your code more concise and readable.Reduced memory leaks: Automatic retain cycle management ensures efficient memory usage and avoids potential crashes.
/// Manual approach
socket.onOpen { [weak self] self?.addMessage("Socket Opened") }
/// Automatic approach
socket.delegateOnOpen(to: self) { (self) in self.addMessage("Socket Opened") }
/// Manual approach
channel.on("new_user") { [weak self] (message) in self?.handle(message) }
/// Automatic approach
channel.delegateOn("new:msg", to: self) { (self, message) in self.handle(message) }
/// Manual approach
channel.join().receive("ok") { [weak self] self?.onJoined() }
/// Automatic approach
channel.join().delegateReceive("ok", to: self, callback: { (self, message) in self.onJoined() }
For detailed real-time messaging API's, refer to Phoenix documentation.
For detailed management of chat and room APIs, refer to Sariska Swagger documentation.
Last updated