Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2022 Swift

WWDC22 · 25 min · Swift

Meet distributed actors in Swift

Discover distributed actors — an extension of Swift’s actor model that simplifies development of distributed systems. We’ll explore how distributed actor isolation and location transparency can help you avoid the accidental complexity of networking, serialization, and other transport concerns when working with distributed apps and systems. To get the most out of this session, watch “Protect mutable state with Swift actors” from WWDC21.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 7 snippets

actor OfflinePlayer swift · at 4:49 ↗
public actor OfflinePlayer: Identifiable {
    nonisolated public let id: ActorIdentity = .random

    let team: CharacterTeam
    let model: GameViewModel
    var movesMade: Int = 0

    public init(team: CharacterTeam, model: GameViewModel) {
        self.team = team
        self.model = model
    }

    public func makeMove(at position: Int) async throws -> GameMove {
        let move = GameMove(
            playerID: id,
            position: position,
            team: team,
            teamCharacterID: team.characterID(for: movesMade))
        await model.userMadeMove(move: move)

        movesMade += 1 
        return move
    }

    public func opponentMoved(_ move: GameMove) async throws {
        do {
            try await model.markOpponentMove(move)
        } catch {
            log("player", "Opponent made illegal move! \(move)")
        }
    }

}
actor BotPlayer swift · at 5:39 ↗
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)
    }
}
distributed actor BotPlayer swift · at 6:11 ↗
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 // first, initialize the implicitly synthesized actor system property
        self.gameState = .init()
        self.ai = RandomPlayerBotAI(playerID: self.id, team: team) // use the synthesized `id` property
    }
    
    public distributed func makeMove() throws -> GameMove {
        return try ai.decideNextMove(given: &gameState)
    }
    
    public distributed func opponentMoved(_ move: GameMove) async throws {
        try gameState.mark(move)
    }
}
Resolving a remote BotPlayer swift · at 12:08 ↗
let sampleSystem: SampleWebSocketActorSystem

let opponentID: BotPlayer.ID = .randomID(opponentFor: self.id)
let bot = try BotPlayer.resolve(id: opponentID, using: sampleSystem) // resolve potentially remote bot player
Server-side actor system app swift · at 13:35 ↗
import Distributed
import TicTacFishShared

/// Stand alone server-side swift application, running our SampleWebSocketActorSystem in server mode.
@main
struct Boot {
    
    static func main() {
        let system = try! SampleWebSocketActorSystem(mode: .serverOnly(host: "localhost", port: 8888))
        
        system.registerOnDemandResolveHandler { id in
            // We create new BotPlayers "ad-hoc" as they are requested for.
            // Subsequent resolves are able to resolve the same instance.
            if system.isBotID(id) {
                return system.makeActorWithID(id) {
                    OnlineBotPlayer(team: .rodents, actorSystem: system)
                }
            }
            
            return nil // unable to create-on-demand for given id
        }
        
        print("========================================================")
        print("=== TicTacFish Server Running on: ws://\(system.host):\(system.port) ==")
        print("========================================================")
        
        try await server.terminated // waits effectively forever (until we shut down the system)
    }
}
Receptionist listing swift · at 20:02 ↗
/// As we are playing for our `model.team` team, we try to find a player of the opposing team
let opponentTeam = model.team == .fish ? CharacterTeam.rodents : CharacterTeam.fish

/// The local network actor system provides a receptionist implementation that provides us an async sequence
/// of discovered actors (past and new)
let listing = await localNetworkSystem.receptionist.listing(of: OpponentPlayer.self, tag: opponentTeam.tag)
for try await opponent in listing where opponent.id != self.player.id {
    log("matchmaking", "Found opponent: \(opponent)")
    model.foundOpponent(opponent, myself: self.player, informOpponent: true)
    // inside foundOpponent:
    // if informOpponent {
    //     Task {
    //         try await opponent.startGameWith(opponent: myself, startTurn: false)
    //     }
    // }

    return // make sure to return here, we only need to discover a single opponent
}
distributed actor LocalNetworkPlayer swift · at 20:23 ↗
public distributed actor LocalNetworkPlayer: GamePlayer {
    public typealias ActorSystem = SampleLocalNetworkActorSystem

    let team: CharacterTeam
    let model: GameViewModel

    var movesMade: Int = 0

    public init(team: CharacterTeam, model: GameViewModel, actorSystem: ActorSystem) {
        self.team = team
        self.model = model
        self.actorSystem = actorSystem
    }

    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
    }

    public distributed func makeMove(at position: Int) async -> GameMove {
        let move = GameMove(
            playerID: id,
            position: position,
            team: team,
            teamCharacterID: movesMade % 2)

        log("player", "Player makes move: \(move)")
        _ = await model.userMadeMove(move: move)

        movesMade += 1
        return move
    }

    public distributed func opponentMoved(_ move: GameMove) async throws {
        do {
            try await model.markOpponentMove(move)
        } catch {
            log("player", "Opponent made illegal move! \(move)")
        }
    }

    public distributed func startGameWith(opponent: OpponentPlayer, startTurn: Bool) async {
        log("local-network-player", "Start game with \(opponent.id), startTurn:\(startTurn)")
        await model.foundOpponent(opponent, myself: self, informOpponent: false)

        await model.waitForOpponentMove(shouldWaitForOpponentMove(myselfID: self.id, opponentID: opponent.id))
    }
}

Resources