Category Archives: Swift

Swift Client for the Asp.NET Core version of SignalR – Part 2: Beyond the Basics

In the previous post we looked at some basic usage of the Swift SignalR Client. This was enough to get started but far from enough for any real-world application. In this post we will look at features offered by the client that allow handling more advanced scenarios.

Lifecycle hooks

One very important detail we glossed over in the previous post was related to starting the connection. While starting the connection seems to be as simple as invoking:

hubConnection.start()
view raw Start.swift hosted with ❤ by GitHub

it is not really the case. If you run the playground sample in one go you will see a lot of errors similar to:

2019-07-29T16:05:00.987Z error: Attempting to send data before connection has been started.

What’s going on here? The start() method is a not blocking call and establishing a connection to the server requires sending some HTTP requests, so takes much more time than just running code locally. As a result, the playground code continues to run and try to invoke hub methods while the client is still working in the background on setting up the connection. Another problem is that there is actually no guarantee that the connection will be ever successfully started (e.g. the provided URL can be incorrect, the network can be down, the server might be not responding etc.) but the start() method never returns whether the operation completed succcessfully. The solution to these problems is the HubConnectionDelegate protocol. It contains a few callbacks that allow the code that consumes the client be notified about the connection lifecycle events. The HubConnectionDelegate protocol looks like this:
public protocol HubConnectionDelegate: class {
func connectionDidOpen(hubConnection: HubConnection)
func connectionDidFailToOpen(error: Error)
func connectionDidClose(error: Error?)
}

The names of the callbacks should make their purpose quite clear but let’s go over them briefly:

  • connectionDidOpen(hubConnection: HubConnection)
    raised when the connection was started successfully. Once this event happens it is safe to invoke hub methods. The hubConnection passed to the callback is the newly started connection
  • connectionDidFailToOpen(error: Error) – raised when the connection could not be started successfully. The error contains the reason of the failure
  • connectionDidClose(error: Error?) – raised when the connection was closed. If the connection was closed due to an error the error argument will contain the reason of the failure. If the connection was closed gracefully (due to calling the stop() method) the error will be nil. Once the connection is closed trying invoking a hub method will result in an error

To set up your code to be notified about hub connection lifecycle events you need to create a class that conforms to the HubConnectionDelegate protocol and use the HubConnectionBuilder.withHubConnectionDelegate() method to register it. One important detail is that the client uses a weak reference to the delegate to prevent retain cycles. This puts the burden of maintaining the reference to the delegate on the user. If the reference is not maintained correctly the delegate might be released prematurely resulting in missing event notifications.
The example chat application shows the usage of the lifecycle events. It blocks/unblocks the UI based on the events raised by hub connection to prevent the user from sending messages when there is no connection to the server. The HubConnectionDelegate derived instance is stored in a class variable to ensure that the delegate will not be released before the connection is stopped.

HubConnectionBuilder

The HubConnectionBuilder is a helper class that contains a number of methods for configuring the connection:

  • withLogging – allows configuring logging. By default no logging will be configured and no logs will be written. There are three overloads of the withLogging method. The simplest overload takes just the minimum log level which can be one of:
    • .debug (= 4)
    • .info (= 3)
    • .warning (= 2)
    • .error (= 1)

    When the client is configured with this overload all log entries at the configured or higher log level will be written using the print function. The user can create more advanced loggers (e.g. a file logger) by creating a class conforming to the Logger protocol and registering it with one of the other withLogging overloads

  • withHubConnectionDelegate – configures a delegate that allows receiving connection lifecycle events (described above)
  • withHttpConnectionOptions – allows setting lower level configuration options (described below)
  • withHubProtocol – used to set the hub protocol that the client will use to communicate with the server. Not very useful at the moment given that currently the only supported hub protocol is the Json hub protocol which is also used by default (i.e. no additional configuration is required to use this protocol)

HttpConnectionOptions

The HttpConnectionOptions class contains lower level configuration options set using the HubConnectionBuilder.withHubConnectionOptions method. It allows configuring the following options:

  • accessTokenProvider – used to set a token provider factory. Each time the client makes an HTTP request (currently – because the client supports only the webSocket transport – this happens when sending the negotiate request and when opening a webSocket) the client will invoke the provided token factory and set the Authorization HTTP header to:
    Bearer {token-returned-by-factory}
  • skipNegotiation – by default the first step the client takes to establish a connection with a SignalR server is sending a negotiate request to get the capabilities of the server (e.g. supported transports), the connection id which identifies the connection on the server side and a redirection URL in case of Azure SignalR Service. However, the webSocket transport does not need a connection id (the connection is persistent) and if the user knows that the server supports the webSocket transport the negotiate request can be skipped saving one HTTP request and thus making starting the connection faster. The default value is false. Note: when connecting to Azure SignalR service this setting must be set to false regardless of the transport used by the client
  • headers – a dictionary containing HTTP headers that should be included in each HTTP request sent by the client
  • httpClientFactory – a factory that allow providing an alternative implementation of the HttpClient protocol. Currently used only by tests

Azure SignalR Service

When working with Azure SignalR Service the only requirement is that the HttpConnectionOptions.skipNegotiation is set to false. This is the default setting so typically no special configuration is required to make this scenario work.

Miscellaneous

Limits on the number of arguments

The invoke/send methods have strongly typed overloads that take up to 8 arguments. This should be plenty but in rare cases when this is not enough it is possible to drop to lower level primitives and use functions that operate on arrays of items that conform to the Encodable protocols. These functions work for any number of arguments and can be used as follows:

hubConnection.invoke(method: "Add", arguments: [2, 3], resultType: Int.self) { result, error in
if let error = error {
print("error: \(error)")
} else {
print("Add result: \(result!)")
}
}

Variable number of arguments

The SignalR server does not enforce that the same client method is always invoked with the same number of arguments. On the client side this rare scenario cannot be handled with the strongly typed .on methods. In addition -similarly to the scenarios described above – there is a limit of 8 parameters that the strongly typed .on callbacks support. Both scenarios can be handled by dropping to the lower level primitive which uses an ArgumentExtractor class instead of separate arguments. Here is an example:

hubConnection.on(method: "AddMessage", callback: { argumentExtractor in
let user = try argumentExtractor.getArgument(type: String.self)
var message = ""
if argumentExtractor.hasMoreArgs() {
message = try argumentExtractor.getArgument(type: String.self)
}
print(">>> \(user): \(message)")
})

These are pretty much all the knobs and buttons that the Swift SignalR Client currently offers. Knowing them allows using the client in the most effective way.

Advertisement

Swift Client for the Asp.NET Core version of SignalR – Part 1: Getting Started

SignalR-Client-Swift is a SignalR client for the Core version of SignalR for applications written in Swift. It’s been around for a while and, although the work is still in progress, it is stable and usable enough to use it in real apps. The project is an open source project hosted on GitHub and has received number of contributions from the community (e.g. including big features like support for Swift Package Manager). Unfortunately, so far, the documentation for this client has been between scarce and non-existent making it harder to adopt it. This and the next post aim to fix this problem.

Before looking diving into code let’s talk about the current state of affairs. As I mentioned, the work is far from finished and some features you can find in other clients are not currently supported. The following is the list of major SignalR features that are currently not implemented:

  • Long Polling and Server Sent Events transports
  • non-Json based hub protocols (e.g. Message Pack)
  • restartable connections
  • KeepAlive messages

Here is the more positive list of major features that are implemented:

  • webSockets transport
  • client and server hub method invocations (using Json hub protocol)
  • streaming methods
  • support for Azure SignalR Service
  • authentication with auth tokens

SignalR for ASP.Net Core is not backwards compatible with the previous version of SignalR. Hence, the Swift SignalR client will not work with the non-Core version of SignalR server.

Installation

The first step to use the client in a project is installation. Currently there are three ways to install Swift SignalR Client into your project:

CocoaPods

Add the following lines to your Podfile:

use_frameworks!
pod 'SwiftSignalRClient'
view raw CocoaPods hosted with ❤ by GitHub

Then run:
pod install

Swift Package Manager (SPM)

Add the following to your Package dependencies:

.package(url: "https://github.com/moozzyk/SignalR-Client-Swift", .upToNextMinor(from: "0.6.0")),

Then include "SignalRClient" in your target dependencies. For example:

.target(name: "MySwiftPackage", dependencies: ["SignalRClient"]),

Manually

Pull the code from the GitHub repo and configure SignalR client as an Embedded Framework.

Usage

Once the client has been successfully installed it is ready to use. The usage of the Swift SignalR Client does not differ much from other existing clients – you need to create a hub connection instance that you will use to connect and talk to the server. Note that you need to use the same instance of the client for the entire lifetime of your connection.
The easiest way to create a HubConnection instance is to use the HubConnectionBuilder class which contains a number of methods that allow configuring the connection to be created. For instance, creating a HubConnection instance with logging configured at the debug level would look like this:

let hubConnection = HubConnectionBuilder(url: URL(string: "http://localhost:5000/playground")!)
.withLogging(minLogLevel: .debug)
.build()

Creating a hub connection does not automatically start the connection. It just creates an instance that will be used to communicate with the server once the connection is started. This pattern makes it possible to register handlers for the client-side methods without risking missing invocations received between starting the connection and registering the handler. Handlers for the client-side methods are registered with the on method as follows:

hubConnection.on(method: "AddMessage") {(user: String, message: String) in
print(">>> \(user): \(message)")
}
view raw On.swift hosted with ❤ by GitHub

It is worth noting that types for the handler parameters must be specified and must be compatible with the types of values sent by the server (e.g. if the server invokes the method with a string the parameter type of the handler cannot be Int). The number of handler parameters should match the number of arguments used to invoke the client-side method from the server side.

After registering handlers it’s time to start the connection. It is as easy* as:

hubConnection.start()
view raw Start.swift hosted with ❤ by GitHub

From this point on, if the connection was started successfully, the handlers for the client-side methods will be invoked whenever the method was invoked on the server. Starting the connection allows also to invoke hub methods on the server side. (Trying to invoke a hub method on a non-started connection results in an error). SignalR supports two kinds of hub methods – regular and streaming. When invoking a regular hub method, the client may choose to be notified when the invocation has completed and receive the result of invocation (if the hub method returned any) or an error in case of an exception. Below are examples of such invocations:

// invoking a hub method and receiving a result
hubConnection.invoke(method: "Add", 2, 3, resultType: Int.self) { result, error in
if let error = error {
print("error: \(error)")
} else {
print("Add result: \(result!)")
}
}
// invoking a hub method that does not return a result
hubConnection.invoke(method: "Broadcast", "Playground user", "Sending a message") { error in
if let error = error {
print("error: \(error)")
} else {
print("Broadcast invocation completed without errors")
}
}
view raw Invoke.swift hosted with ❤ by GitHub

When invoking a hub method that returns a result providing the type of the result is mandatory and this type has to be compatible with the type of the value returned by the hub method. Also, there is no distinction between local and remote handlers – i.e. the completion handler will be called with an error not only when the method on the server side fails but also when initiating the invocation fails (e.g. when trying to invoke a method when the connection is not running).

Hub methods can also be invoked in a fire-and-forget manner. When invoking a hub method in this fashion the client will not be notified when the invocation has completed and will not receive any further events related to this method – be it a result or an error. The code below shows how to invoke a hub method in a fire-and-forget manner:

hubConnection.send(method: "Broadcast", "Playground user", "Testing send") { error in
if let error = error {
print("Send failed: \(error)")
}
}
view raw Send.swift hosted with ❤ by GitHub

Note, that the send method still takes a callback that allows handling errors but this callback will be called only for local errors – i.e. errors that occurred when sending data to the server.

SignalR streaming hub methods return a (possibly infinite) stream of items. Each time the client receives a new stream item a user provided callback will be invoked with the received value.
When a streaming method completes executing a completion callback will be invoked (except for this bug which I found writing this post). The client method that invokes streaming hub methods returns a stream handle. This handle can be used to cancel the streaming hub method. The following code snippet illustrates how to invoke and cancel a streaming hub method:

let streamHandle = hubConnection.stream(method: "StreamNumbers", 1, 10000, itemType: Int.self,
streamItemReceived: { item in print(">>> \(item!)") }) { error in
print("Stream closed.")
if let error = error {
print("Error: \(error)")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
hubConnection.cancelStreamInvocation(streamHandle: streamHandle) { error in
print("Canceling stream invocation failed: \(error)")
}
}
view raw Stream.swift hosted with ❤ by GitHub

If you no longer want to receive notifications from the server or invoke hub methods you can disconnect from the server with:

hubConnection.stop()
view raw Stop.swift hosted with ❤ by GitHub

One final note about types of arguments and results. The types of all the values sent to the server must conform to the Encodable protocol. The types for the values returned from the server must conform to the Decodable protocol. The most common types in Swift already conform to the Codable protocol (which means that they conform to both the Encodable and the Decodable protocols) and when creating custom structs/classes it is easy to make them conform to the Codable protocol as long as all the member variables already conform to the Codable protocol.

These are the basics of the SignalR Swift Client. The project repo contains additional resources in form of example applications for macOS and iOS. I also created a Swift playground which contains all code snippets published in this post. In the next post we will look at the connection lifecycle events, available configuration options and more advanced scenarios.

* – it is actually not entirely true but we will return to it in the second post†