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

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

  1. […] 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 […]

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: