- Single-actor client.
DiscordClientowns the gateway connection, REST client, event dispatcher, and cache in one place. - Automatic gateway management. Reconnection, session resumption, heartbeats, and rate-limit backoff are handled internally.
- Actor-based concurrency. Built on Swift 6.2 structured concurrency with
async/await. The cache, gateway, and REST client avoid locks by design. - Typed models. Every Discord API object is a native Swift struct with
Codable,Hashable, andSendable. The REST client returns concrete types, not[String: Any]. - Event callbacks + async streams. Use callback closures or
for awaiton the unified event stream — or both at once. - Pluggable transports. Default transports use URLSession. Swap in AsyncHTTPClient for proxy support on Linux, or conform to
HTTPTransport/WebSocketTransportfor custom networking. - Modular high-level APIs. Separate routers for slash commands, prefix commands, autocomplete, views, webhooks, collectors, and cooldowns.
Create a new SwiftPM executable, add SwiftDisc as a dependency, and drop this in:
import Foundation
import SwiftDisc
@main
struct MyBot {
static func main() async {
let token = ProcessInfo.processInfo.environment["DISCORD_BOT_TOKEN"] ?? ""
let client = DiscordClient(token: token)
await client.setOnReady { ready in
print("Logged in as \(ready.user.username)")
}
await client.setOnMessage { message in
guard message.content?.lowercased() == "ping" else { return }
do {
try await message.reply(client: client, content: "Pong!")
} catch {
print("Reply failed: \(error)")
}
}
do {
try await client.loginAndConnect(intents: [.guilds, .guildMessages, .messageContent])
let events = await client.events
for await _ in events { }
} catch {
print("Client failed: \(error)")
}
}
}Set your token and run:
export DISCORD_BOT_TOKEN="your_token_here"
swift runimport Foundation
import SwiftDisc
@main
struct SlashBot {
static func main() async {
let token = ProcessInfo.processInfo.environment["DISCORD_BOT_TOKEN"] ?? ""
let client = DiscordClient(token: token)
let slash = SlashCommandRouter()
await slash.register("ping") { ctx in
try await ctx.client.createInteractionResponse(
interactionId: ctx.interaction.id,
token: ctx.interaction.token,
content: "Pong!"
)
}
await client.useSlashCommands(slash)
try? await client.loginAndConnect(intents: [.guilds])
let events = await client.events
for await _ in events { }
}
}Add SwiftDisc via Swift Package Manager:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "YourBot",
dependencies: [
.package(url: "https://github.com/M1tsumi/SwiftDisc.git", from: "2.5.0")
],
targets: [
.target(
name: "YourBot",
dependencies: ["SwiftDisc"]
)
]
)| Platform | Minimum version |
|---|---|
| macOS | 11+ |
| iOS | 14+ |
| tvOS | 14+ |
| watchOS | 7+ |
| Windows | Swift 6.2+ |
SwiftDisc is split into these layers:
DiscordClient -- the main actor. It owns your Gateway connection, the REST client, the event dispatcher, and the cache. You will spend most of your time here.
Gateway -- manages the WebSocket connection to Discord's real-time event system. Handles heartbeats, reconnects, resuming sessions, and rate limits so you do not have to. Monitor connection state with client.connectionState and react to lifecycle events with onResumed, onDisconnected, and onSessionInvalidated.
REST client -- typed HTTP methods for every Discord API endpoint. Methods throw DiscordError with descriptive messages, and the built-in rate limiter keeps you under Discord's global and per-route limits.
High-level modules in Sources/SwiftDisc/HighLevel:
| Module | What it does |
|---|---|
SlashCommandRouter |
Register and handle slash commands with typed option accessors |
AutocompleteRouter |
Provide live search suggestions for command options |
CommandRouter |
Classic prefix-based commands (e.g. !ping) |
ViewManager |
Persistent UI views with custom_id matching |
WebhookClient |
Standalone token-free webhook execution |
MessagePayload |
Fluent builder for complex message payloads |
Collectors |
Streams for messages, reactions, and components |
CooldownManager |
Per-user or per-guild rate limiting for commands |
Models in Sources/SwiftDisc/Models -- complete, typed Swift structs for every Discord API object, from Users and Channels to Interactions, Polls, Auto Moderation, Monetization SKUs, and Onboarding prompts.
Cache -- actor-safe in-memory store for users, channels, guilds, roles, emojis, and recent messages. Supports TTL expiration and LRU eviction to keep memory bounded. Inspect cache health with cache.summary, cache.userCount, etc.
The default transport uses URLSession and works everywhere. If you need proxy support on Linux or want to use AsyncHTTPClient, swap in the optional SwiftDiscAHCTransport module:
import SwiftDisc
import SwiftDiscAHCTransport
var config = DiscordConfiguration()
config.httpTransport = AHCTransport(
proxy: ProxyConfiguration(host: "proxy.corp.com", port: 8080)
)
let client = DiscordClient(token: token, configuration: config)Conform to HTTPTransport or WebSocketTransport to integrate any networking library.
Run any example from the repo root with swift run <TargetName>:
| Example | What it shows | Run with |
|---|---|---|
| PingBot | Minimal message-based bot | swift run PingBotExample |
| SlashBot | Slash command registration + response | swift run SlashBotExample |
| CommandsBot | Prefix commands (!ping style) |
swift run CommandsBotExample |
| AutocompleteBot | Slash commands with live search suggestions | swift run AutocompleteBotExample |
| ComponentsExample | Buttons, select menus, and interaction handling | swift run ComponentsExample |
| ComponentsV2Bot | The IS_COMPONENTS_V2 flag, channel select menus, modal with Label+TextInput layout |
swift run ComponentsV2BotExample |
| ViewExample | Persistent UI views with ViewManager | swift run ViewExample |
| FileUploadBot | Sending attachments with embeds | swift run FileUploadBotExample |
| WebhookBot | Create, execute, edit, and delete webhooks | swift run WebhookBotExample |
| ShardingBot | Sharded gateway connection with state monitoring | swift run ShardingBotExample |
All examples read the bot token from the DISCORD_BOT_TOKEN environment variable. Some also use DISCORD_CHANNEL_ID -- set them before running:
export DISCORD_BOT_TOKEN="your_token"
export DISCORD_CHANNEL_ID="your_channel_id"
swift run PingBotExampleYou can handle gateway events with callbacks or an async stream.
Set individual callbacks for the events you care about:
await client.setOnReady { ready in
print("Bot is ready in \(ready.guilds.count) guilds")
}
await client.setOnMessage { message in
guard message.content == "ping" else { return }
try await message.reply(client: client, content: "Pong!")
}
await client.setOnGuildCreate { guild in
print("Joined \(guild.name), \(guild.member_count ?? 0) members")
}Available callbacks: onReady, onMessage, onMessageUpdate, onGuildCreate, onInteractionCreate, onReactionAdd, onMemberAdd, and 30+ more -- one for every Discord gateway event.
When you need to filter, combine, or transform events, use the unified stream:
for await event in await client.events {
switch event {
case .messageCreate(let msg) where msg.content == "ping":
try await msg.reply(client: client, content: "Pong!")
case .guildCreate(let guild):
print("New guild: \(guild.name)")
case .reactionAdd(let reaction):
guard reaction.emoji.name == "⭐" else { break }
print("Starred in channel \(reaction.channel_id)")
default:
break
}
}The event stream is an AsyncStream -- you can use it with filter, map, compactMap, and other async algorithms. You can also use both callbacks and the stream at the same time.
Monitor Gateway connection state in real time:
for await state in await client.connectionState {
switch state {
case .ready: print("Connected and ready to receive events")
case .reconnecting: print("Connection lost, reconnecting...")
case .resuming: print("Resuming session with Discord")
case .disconnected: print("Fully disconnected")
case .connecting: print("Establishing initial connection")
case .identifying: print("Sending identify payload")
}
}Or grab the current state synchronously: let state = await client.gatewayStatus.
React to connection lifecycle events:
await client.onResumed = { print("Session resumed -- missed events replayed") }
await client.onDisconnected = { reason in print("Disconnected: \(reason)") }
await client.onSessionInvalidated = { print("Session invalidated -- will re-identify on next connect") }If your bot connects but does not receive events, check these in order:
- Is your token set? --
DISCORD_BOT_TOKENmust be a valid bot token from the Discord Developer Portal. - Are you requesting the right intents? -- Pass them to
loginAndConnect(intents:). For example, to read message content you need[.guilds, .guildMessages, .messageContent]. - Are privileged intents enabled? -- In the Developer Portal, go to your app's Bot page and toggle
MESSAGE CONTENT INTENT,SERVER MEMBERS INTENT, andPRESENCE INTENTas needed. These are required even if you pass them in code. - Was the bot invited with the right scopes? -- Use the OAuth2 URL generator in the Developer Portal and include the
botscope plus the permissions your bot needs. - Is the bot in the guild? -- The bot must be a member of the guild to receive events from it.
- Check for close codes -- Watch console output for Gateway close codes like
4004(bad token),4013(invalid intents), or4014(disallowed privileged intent).
Reconnection, rate-limit backoff, and session resumption are handled internally. Debugging tools:
Gateway decode diagnostics -- Enable DiscordConfiguration.enableGatewayDecodeDiagnostics to log payload decoding failures with opcode context and payload previews. Essential when adding support for new Discord features.
Rate limit observability -- Set DiscordConfiguration.onRateLimit to receive RateLimitEvent snapshots for REST bucket updates and waits. Useful for tuning request patterns.
Structured logging -- Provide a custom logger via DiscordConfiguration.logger. The built-in DefaultDiscordLogger uses os_log on Apple platforms and print on others. Implement the DiscordLogger protocol to route to your own backend.
Pluggable HTTP and WebSocket transports -- Swap out the default URLSession networking for a custom implementation. Useful when you need proxy support on Linux, want to use AsyncHTTPClient, or need fine-grained control over connection behavior.
Typed error handling -- All operations throw DiscordError with descriptive messages. Use convenience properties to inspect errors:
catch let error as DiscordError {
if error.isRateLimited { /* back off */ }
if error.isAuthenticationFailure { /* token expired */ }
if let statusCode = error.httpStatusCode { /* 400, 404, 429 etc */ }
if let validationErrors = error.validationErrors { /* per-field failures */ }
}Router error handlers -- CommandRouter, SlashCommandRouter, and ViewManager support custom error handlers that receive context about the failed operation. Set them during initialization.
Cache statistics -- Inspect cache contents any time:
print(await cache.summary)
// "Cache: 843 users, 127 channels, 5 guilds, 3412 messages in 34 channels, ..."| Resource | What you will find |
|---|---|
| GitHub Pages | DocC documentation for SwiftDisc -- API reference with search |
| CHANGELOG.md | Per-release changelog following Keep a Changelog |
| CONTRIBUTING.md | How to set up, build, test, and submit PRs |
| Examples/README.md | Quick-start guides for every example bot |
SwiftDiscAHCTransport |
Optional AsyncHTTPClient transport. Add .product(name: "SwiftDiscAHCTransport", package: "SwiftDisc") to use it. Supports proxies on Linux |
| CODE_OF_CONDUCT.md | Community standards and expectations |
You can also build the docs locally:
# Requires swift-docc-plugin (add it to Package.swift first)
swift package --allow-writing-to-directory generate-documentation --target SwiftDisc --output-path docs --transform-for-static-hostingThen open docs/index.html in a browser.
- Discord server -- https://discord.gg/tWyefRKKEH -- get help, discuss features, show off your bot
- GitHub Issues -- https://github.com/M1tsumi/SwiftDisc/issues -- report bugs and request features
- GitHub Discussions -- available on the repo for longer conversations
MIT. See LICENSE.