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() |
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 connectionconnectionDidFailToOpen(error: Error)
– raised when the connection could not be started successfully. The error contains the reason of the failureconnectionDidClose(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 thestop()
method) the error will benil
. 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 thewithLogging
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 theLogger
protocol and registering it with one of the otherwithLogging
overloadswithHubConnectionDelegate
– 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 theAuthorization
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 isfalse
. Note: when connecting to Azure SignalR service this setting must be set tofalse
regardless of the transport used by the clientheaders
– a dictionary containing HTTP headers that should be included in each HTTP request sent by the clienthttpClientFactory
– a factory that allow providing an alternative implementation of theHttpClient
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.