WWDC Quick Look 💓 By SwiftGGTeam
Meet distributed actors in Swift

Meet distributed actors in Swift

Watch original video

Highlight

Swift 5.7’s distributed actor extends the actor model from single process to multi-process scenarios. Through the serialization requirements and positional transparency checked by the compiler, remote calls and local calls are written in exactly the same way.

Core Content

From local actor to distributed actor

Your Tic-Tac-Toe game currently only has a single-player mode. Both the human player and the AI ​​opponent are local actors communicating via asynchronous messages. You want to add a multiplayer feature that allows players on two devices to connect with each other.

The previous approach was to manually handle WebSocket connections, message serialization, and state synchronization. The code is full ofencode/decode, reconnection logic, heartbeat detection, and game logic are overwhelmed by network code.

(00:30) The core idea of ​​a distributed actor is: the actor already communicates through message passing, why can’t these messages be sent to another device?

In Swift actor’s “sea of ​​concurrency” model, each actor is an isolated island, exchanging data through asynchronous messages. Distributed actors extend this model into a “distributed sea”: each island can be located on a different device, server or process.

The first step of migration: change Actor into Distributed Actor

(04:47) First look at the original local actor:

public actor BotPlayer: Identifiable {
    nonisolated public let id: ActorIdentity = .random

    var ai: RandomPlayerBotAI
    var gameState: GameState

    public init(team: CharacterTeam) {
        self.gameState = .init()
        self.ai = RandomPlayerBotAI(playerID: self.id, team: team)
    }

    public func makeMove() throws -> GameMove {
        return try ai.decideNextMove(given: &gameState)
    }

    public func opponentMoved(_ move: GameMove) async throws {
        try gameState.mark(move)
    }
}

(06:11) Changing to distributed actor only requires a few steps:

import Distributed

public distributed actor BotPlayer: Identifiable {
    typealias ActorSystem = LocalTestingDistributedActorSystem

    var ai: RandomPlayerBotAI
    var gameState: GameState

    public init(team: CharacterTeam, actorSystem: ActorSystem) {
        self.actorSystem = actorSystem
        self.gameState = .init()
        self.ai = RandomPlayerBotAI(playerID: self.id, team: team)
    }

    public distributed func makeMove() throws -> GameMove {
        return try ai.decideNextMove(given: &gameState)
    }

    public distributed func opponentMoved(_ move: GameMove) async throws {
        try gameState.mark(move)
    }
}

Key points:

  • distributed actorsubstituteactor, automatically matchesDistributedActorprotocol -ActorSystemA type alias declares the distributed system used by this actor -actorSystemIt is a property synthesized by the compiler and must be assigned in the initializer. -idProperties are automatically assigned by the actor system and cannot be declared manually -distributed funcMark methods that can be called remotely

Location transparency

(03:41) The most important feature of a distributed actor is location transparency. Whether the actor is local or remote, the calling code is exactly the same:

// Local actor
let localBot = BotPlayer(team: .fish, actorSystem: system)
let move = try await localBot.makeMove()

// Remote actor (get a reference via resolve)
let remoteBot = try BotPlayer.resolve(id: opponentID, using: webSocketSystem)
let move = try await remoteBot.makeMove()  // The syntax is exactly the same!

Key points:

  • resolve(id:using:)Get a reference to a remote actor, not create an instance
  • The returned reference uses exactly the same API as the local instance
  • Networking, serialization, and transmission details are all handled by the actor system

Server-side implementation

(13:35) Move BotPlayer to run on the server, and the client connects through WebSocket:

import Distributed
import TicTacFishShared

@main struct Boot {
    static func main() {
        let system = try! SampleWebSocketActorSystem(
            mode: .serverOnly(host: "localhost", port: 8888)
        )

        system.registerOnDemandResolveHandler { id in
            if system.isBotID(id) {
                return system.makeActorWithID(id) {
                    OnlineBotPlayer(team: .rodents, actorSystem: system)
                }
            }
            return nil
        }

        try await server.terminated
    }
}

Key points:

  • registerOnDemandResolveHandlerCreate actors on demand
  • The ID pointed to by the message sent by the client may not yet have a corresponding instance.
  • The server determines whether a new BotPlayer needs to be created based on the ID characteristics

Local network battle: Receptionist mode

(16:32) In addition to client/server mode, distributed actors also support peer-to-peer communication. In a local Wi-Fi environment, the two devices discover each other directly and establish a game.

Discover the remote actor using receptionist mode:

let opponentTeam = model.team == .fish ? CharacterTeam.rodents : CharacterTeam.fish

let listing = await localNetworkSystem.receptionist.listing(
    of: OpponentPlayer.self,
    tag: opponentTeam.tag
)

for try await opponent in listing where opponent.id != self.player.id {
    model.foundOpponent(opponent, myself: self.player, informOpponent: true)
    return
}

Key points:

  • receptionist.listing(of:tag:)Returns an AsyncSequence
  • When a new device joins the network, the listing will automatically generate a new value
  • usefor try awaitTraverse discovered opponents

(20:23) When human players act as distributed actors, they need to handle remote calls to the local UI:

public distributed actor LocalNetworkPlayer: GamePlayer {
    public typealias ActorSystem = SampleLocalNetworkActorSystem

    let team: CharacterTeam
    let model: GameViewModel

    public distributed func makeMove() async -> GameMove {
        let field = await model.humanSelectedField()
        movesMade += 1
        let move = GameMove(
            playerID: self.id,
            position: field,
            team: team,
            teamCharacterID: movesMade % 2
        )
        return move
    }
}

Key points:

  • makeMove()When called remotely, it needs to wait for human players to operate on the local UI -await model.humanSelectedField()Suspend remote call until user clicks
  • returnedGameMoveAutomatic serialization back to remote caller via actor system

Detailed Content

Compiler checks for Distributed Actors

Distributed actors have three more layers of compiler checks than ordinary actors:

// 1. Only distributed methods can be called remotely
public distributed func makeMove() throws -> GameMove  // OK
public func helper() { }  // Cannot be called on a remote reference

// 2. Parameters and return values must meet serialization requirements
distributed func sendMessage(_ text: String)  // OK, String is Codable
distributed func sendImage(_ image: UIImage)  // Compiler error! UIImage is not Codable

// 3. Actor system types must match
let bot: BotPlayer  // BotPlayer's ActorSystem is LocalTestingDistributedActorSystem
let remoteBot = try BotPlayer.resolve(id: id, using: otherSystem)  // Type mismatch causes an error

Key points:

  • distributedKeywords explicitly mark remote API boundaries
  • The default serialization requirement isCodable, the actor system can be customized
  • The compiler ensures the type safety of remote calls at compile time

Responsibilities of Actor System

Actor system is the core abstraction of distributed actors and is responsible for:

  1. Assign ID: Each actor is assigned a globally unique identifier when it is created.
  2. Serialization/Deserialization: Encode method calls into network messages
  3. Routing: Find the location of the target actor based on the ID
  4. Transmission: Send messages over the network
// Local testing system built into Swift 5.7
typealias ActorSystem = LocalTestingDistributedActorSystem

// Custom WebSocket system
typealias ActorSystem = SampleWebSocketActorSystem

// Open-source cluster system (SwiftNIO implementation)
// https://github.com/apple/swift-distributed-actors/

Serialization requirements

(10:21) All of Distributed actorsdistributed funcThe parameter and return value types must meet the serialization requirements of the actor system. The default isCodable

struct GameMove: Codable {
    let playerID: ActorIdentity
    let position: Int
    let team: CharacterTeam
    let teamCharacterID: Int
}

If the actor system uses other serialization schemes (such as Protocol Buffers), different requirements can be declared:

distributed actor BotPlayer {
    typealias ActorSystem = ProtobufActorSystem
    // At this point, parameters and return values must conform to ProtobufSerializable
}

Core Takeaways

1. Use distributed actors instead of manual network layers

If you are using WebSockets, URLSession, or third-party libraries for inter-device communication, consider refactoring using distributed actors. It is particularly suitable for scenarios such as game status synchronization, collaborative editing, and real-time chat. fromLocalTestingDistributedActorSystemInitially, after passing the local test, connect to the real network.

Entrance:import Distributeddistributed actor

2. Build a local multiplayer experience

Leverage the Network framework + distributed actors to implement local multiplayer games with zero server costs. Receptionist mode automatically discovers nearby devices without requiring users to manually enter IP addresses or scan QR codes.

Entrance:receptionist.listing(of:tag:)SampleLocalNetworkActorSystem

3. Use actor system to abstract different deployment modes

The same set of game logic can be deployed using different actor systems: for local testingLocalTestingDistributedActorSystem, the custom Network framework system is used for local area network battles, the WebSocket system is used for online battles, and the Swift Distributed Actors library is used for large-scale clusters. The business code does not need to be changed at all.

Entrance:typealias ActorSystem = ...resolve(id:using:)

4. A new choice for server-side Swift

Distributed actors provide a type-safe RPC abstraction for server-side Swift. Compared with Vapor’s route processor, distributed actors allow clients and servers to share interface definitions, and the compiler ensures that the types on both ends are consistent.

Entrance:SampleWebSocketActorSystem(mode: .serverOnly)registerOnDemandResolveHandler

  • What’s new in Swift — Overview of new features in Swift 5.7, including language-level support for distributed actors
  • Meet Swift Async Algorithms — an asynchronous algorithm library that can work with distributed actors to process real-time data streams
  • Design protocol interfaces in Swift — Swift 5.7some/anySyntax and primary association types, the API design of distributed actors benefits from these improvements

Comments

GitHub Issues · utterances