A sample project that shows how to use WebSocketActors to create a simple client/server application using Swift distributed actors.
The server stores a single global counter. The client displays that counter, and provides a button to increment the counter. All clients display the same counter, and are updated immediately when the counter changes.
This repository includes:
- A Swift Package Manager library in
Sources/Monotonic
that contains the distributed actors that are shared by the client and the server. - A Swift Package Manager command-line executable in
Sources/Server
that implements the server. The server runs on macOS and Linux. - Scripts to build a Docker image from the server executable.
- An iOS client application in
MonotonicApp
that communicates with the server.
When you are developing an iOS client/server application, the interface of your distributed actors becomes your server's API, and you need to ensure that the client and server APIs stay in sync. This is easist if your client and server code are stored in the same repository and built together. To do this in Xcode:
- Create a Swift Package Manager project for your shared library and server executable.
- Publish this package to some place accessible to the Swift Package Manager, like Github.
- Create an Xcode project for your iOS client.
- Add a package dependency from your iOS client application to the library in your Swift package.
- Move the folder containing your Xcode project into your package folder.
- Create an empty Xcode workspace in the package folder.
- Add both the package folder and the Xcode project to the workspace.
From then on, do all of your development using the workspace. All distributed actor code should go in the shared library. You can run and debug the client and server simultaneously in Xcode by switching to the server schema, running the server, switching to the client schema, and running the client.
The Swift distributed actor system is designed to be symmetric: all nodes in the actor system use the same actors regardless of whether they are local or remote. That makes life easy in a peer-to-peer cluster like Apple's distributed actor system, but is more awkward in a client/server system like WebSocketActors.
To separate client code from server code, I recommend delegating most of the
work inside your distributed actors to a protocol, and them implementing the
protocol within your client or server. You can see this in the
CountMonitor
actor, which delegates to the LocalModel
protocol. This protocol is
implemented in the client, but the server does not need this implementation in
order to call CountMonitor
actors on the client.
During development it's easy to keep the client and server APIs in sync because they are short-lived and running from the same source code. But once you start distributing your application to others, you have to ensure that you don't break your APIs when you update the code.
One way to do this is to deploy different servers for different versions of your
code, but this is rarely practical. To maintain compatibility you will need to
restrict the kinds of changes you make to your distributed actors and the
Sendable
data that is passed between them. Here are some guidelines to
maintain backward compatibillity:
- Don't change the name of your distributed actors. If you need to make a name change, create a new actor with the new name and continue to maintain the previous actor under the old name.
- Don't change the name, arguments, argument labels, or return types of your distributed functions. You can add new distributed functions, or new overloads for existing functions, but the original functions must remain intact.
- Maintain JSON compatibility for any
Sendable
data passed to or from distributed functions. From the beginning, customize theDecodable
implementation for yourSendable
data to ignore any unrecognized properties and provide default values for any missing properties.