Meet SwiftDisc — a Swift-first Discord API wrapper that feels like it was written for you, not by a spec sheet. It brings together the full Discord REST API, Gateway events, typed models, and high-level bot-building tools — all built on Swift 6.2's async/await and with actor-safe concurrency from day one.
Whether you're writing your first ping-pong bot or a sharded moderation platform, SwiftDisc gives you the building blocks without getting in your way.
- Quick start
- Installation
- How SwiftDisc works
- Example programs
- Event handling guide
- Bot setup checklist
- Reliability and debugging
- Documentation map
- Community and support
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.4.0")
],
targets: [
.target(
name: "YourBot",
dependencies: ["SwiftDisc"]
)
]
)| Platform | Minimum version |
|---|---|
| macOS | 11+ |
| iOS | 14+ |
| tvOS | 14+ |
| watchOS | 7+ |
| Windows | Swift 6.2+ |
SwiftDisc is organized into a few clear layers so you can grab what you need without digging through a maze:
DiscordClient — the main actor. It owns your Gateway connection, the REST client, the event dispatcher, and the cache. You'll 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 don't 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:
SlashCommandRouter— register and handle slash commands with typed option accessorsAutocompleteRouter— provide live search suggestions for command optionsCommandRouter— classic prefix-based commands (e.g.!ping)ViewManager— persistent UI views withcustom_idmatchingWebhookClient— standalone token-free webhook executionMessagePayload— fluent builder for complex message payloadsCollectors— streams for messages, reactions, and componentsCooldownManager— 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.
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 PingBotExampleSwiftDisc gives you two ways to handle gateway events. Pick the one that fits your style.
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 doesn't 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).
SwiftDisc is built to be resilient by default — automatic reconnection, rate-limit backoff, and session resumption are all handled internally. When you need to look under the hood:
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 behaviour.
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)The default URLSession transport is used when no custom transport is provided — no code changes needed for existing bots. Conform to HTTPTransport or WebSocketTransport to integrate any networking library.
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'll find |
|---|---|
| GitHub Pages | Auto-generated DocC documentation for SwiftDisc — browseable API reference with search |
| CHANGELOG.md | Detailed 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 in-tree AsyncHTTPClient transport. Add .product(name: "SwiftDiscAHCTransport", package: "SwiftDisc") to use it. Includes native proxy support |
| CODE_OF_CONDUCT.md | Community standards and expectations |
- Discord server — https://discord.gg/6nS2KqxQtj — 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.