A modern Swift library for building Discord bots and integrations.
Built with async/await, strongly typed, and cross-platform.
SwiftDisc is a powerful Swift library for interacting with the Discord API. It embraces modern Swift concurrency with async/await throughout, provides fully typed models for Discord's data structures, and handles common pain points like rate limiting, reconnection, and sharding automatically.
Whether you're building a simple bot or a complex integration, SwiftDisc gives you the tools you need while staying out of your way.
Add SwiftDisc to your Swift package dependencies in Package.swift:
dependencies: [
.package(url: "https://github.com/M1tsumi/SwiftDisc.git", from: "2.0.0")
]Then add it to your target:
targets: [
.target(name: "YourBot", dependencies: ["SwiftDisc"])
]| Platform | Minimum Version |
|---|---|
| macOS | 11.0+ |
| iOS | 14.0+ |
| tvOS | 14.0+ |
| watchOS | 7.0+ |
| Windows | Swift 5.9+ |
Here's a simple bot that responds to messages using the v2.0 callback API:
import SwiftDisc
@main
struct MyBot {
static func main() async {
let token = ProcessInfo.processInfo.environment["DISCORD_BOT_TOKEN"] ?? ""
let client = DiscordClient(token: token)
// Assign callbacks — no switch statement needed
client.onReady = { info in
print("✅ Logged in as \(info.user.username)")
}
client.onMessage = { message in
guard message.content == "!ping" else { return }
// reply() sets message_reference automatically
try? await message.reply(client: client, content: "🏓 Pong!")
}
do {
try await client.loginAndConnect(intents: [.guilds, .guildMessages, .messageContent])
} catch {
print("❌ Error: \(error)")
}
}
}Or use typed filtered streams when you need an event loop:
for await message in await client.messageEvents() {
if message.content == "!ping" {
try? await message.reply(client: client, content: "🏓 Pong!")
}
}- Gateway Connection: WebSocket with automatic heartbeat, session resume, and event streaming
- REST API: Comprehensive coverage of Discord's HTTP endpoints
- Rate Limiting: Automatic per-route and global rate limit handling
- Sharding: Built-in support for large bots with health monitoring
- Type Safety: Strongly typed models throughout with compile-time safety
- Cross-Platform: Works on macOS, iOS, tvOS, watchOS, and Windows
- Command Framework: Built-in router for prefix and slash commands
- Component Builders: Fluent API for buttons, select menus, embeds, and modals
- View Manager: Persistent UI views with automatic lifecycle management
- Collectors: AsyncStream-based message and component collectors
- Extensions/Cogs: Modular architecture for organizing bot features
- Utilities: Mention formatters, emoji helpers, timestamp formatting, and more
message.reply()— reply to any message in one line, mention control includedclient.sendDM()— open a DM and send a message in a single call- Typed slash option accessors —
ctx.user(),ctx.channel(),ctx.role(),ctx.attachment() - Filtered event streams —
client.messageEvents(),client.interactionEvents(), etc. EmbedBuilder.timestamp(Date)— passDate()directly, no ISO 8601 string needed- Public
CooldownManager— use it anywhere in your bot, not just command routers - 32 event callbacks — one
@Sendableclosure per event, noswitchboilerplate - Background cache eviction — TTL expiry runs automatically, no manual calls needed
The REST API covers all essential Discord features:
✅ Messages, embeds, reactions, threads
✅ Channels, permissions, webhooks
✅ Guilds, members, roles, bans
✅ Slash commands, autocomplete, modals
✅ Components (buttons, select menus, radio groups, checkbox groups)
✅ Modal components: Label, RadioGroup, CheckboxGroup, Checkbox
✅ Scheduled events, stage instances
✅ Auto-moderation rules
✅ Application commands and interactions
✅ Gradient role colors and guild tags
✅ Voice state REST endpoints
✅ Community invite target user management
For a complete API checklist, see the REST API Coverage section below.
Create command-based bots easily with the built-in router:
let router = CommandRouter(prefix: "!")
router.register("ping") { ctx in
try? await ctx.reply("Pong!")
}
client.onMessageCreate { message in
await router.processMessage(message)
}Add checks and cooldowns to commands:
router.register("ban", checks: [isAdminCheck], cooldown: 10.0) { ctx in
// Command logic here
}let slash = SlashCommandRouter()
slash.register("greet") { interaction in
try await interaction.reply("Hello from SwiftDisc!")
}Use the fluent builders to create rich messages:
let embed = EmbedBuilder()
.title("Welcome!")
.description("Thanks for joining our server")
.color(0x5865F2)
.timestamp(Date())
.build()
let button = ButtonBuilder()
.style(.primary)
.label("Click me!")
.customId("welcome_button")
.build()
let row = ActionRowBuilder()
.addButton(button)
.build()
try await client.sendMessage(
channelId: channelId,
embeds: [embed],
components: [row]
)Collect messages or component interactions using AsyncStreams:
let collector = client.createMessageCollector(
filter: { $0.author.id == userId && $0.channel_id == channelId },
timeout: 60.0,
max: 1
)
for await message in collector {
print("Received: \(message.content)")
}Create persistent interactive UIs with automatic lifecycle management:
let view = BasicView(timeout: 300) { customId, interaction in
if customId == "confirm_button" {
try? await interaction.reply("Confirmed!")
return true // Remove view after use
}
return false
}
client.viewManager?.register(view: view, for: messageId)Organize your bot into modular extensions:
struct ModerationCog: Cog {
func onLoad(client: DiscordClient) async {
print("Moderation module loaded")
// Register commands, set up listeners
}
func onUnload(client: DiscordClient) async {
print("Moderation module unloaded")
}
}
let extensionManager = ExtensionManager()
await extensionManager.load(cog: ModerationCog(), client: client)For large bots, SwiftDisc handles sharding automatically:
let manager = await ShardingGatewayManager(
token: token,
configuration: .init(
shardCount: .automatic,
connectionDelay: .staggered(interval: 1.5)
),
intents: [.guilds, .guildMessages]
)
try await manager.connect()
let health = await manager.healthCheck()
print("Shards: \(health.readyShards)/\(health.totalShards) ready")Connect to voice channels and send audio:
// Enable voice in config
let config = DiscordConfiguration(enableVoiceExperimental: true)
let client = DiscordClient(token: token, configuration: config)
// Join a voice channel
try await client.joinVoice(guildId: guildId, channelId: voiceChannelId)
// Send Opus audio
try await client.playVoiceOpus(guildId: guildId, data: opusPacket)
// Or use an audio source
try await client.play(source: audioSource, guildId: guildId)Send files with messages and interactions:
let file = FileAttachment(
filename: "image.png",
data: imageData,
contentType: "image/png"
)
try await client.sendMessage(
channelId: channelId,
content: "Check out this image!",
files: [file]
)For interaction responses:
try await client.createInteractionResponseWithFiles(
applicationId: appId,
interactionToken: token,
payload: responsePayload,
files: [file]
)SwiftDisc includes helpful utilities for common tasks:
// Mention formatting
Mentions.user(userId) // <@123456>
Mentions.channel(channelId) // <#123456>
Mentions.role(roleId) // <@&123456>
// Custom emoji
EmojiUtils.custom(name: "party", id: emojiId, animated: true)
// Timestamp formatting
DiscordTimestamp.format(date: Date(), style: .relative)
// Escape special characters
MessageFormat.escapeSpecialCharacters(userInput)✅ Send, edit, delete messages
✅ Reactions (add, remove, remove all)
✅ Embeds, components, attachments
✅ Pins, bulk delete
✅ Crosspost, polls
✅ Forward messages
✅ Create, modify, delete channels
✅ Permissions, invites
✅ Webhooks (full CRUD + execute)
✅ Typing indicators
✅ Threads (create, archive, members)
✅ Create, modify, delete guilds
✅ Channels, roles, emojis
✅ Members (add, remove, modify, timeout)
✅ Bans, prune, audit logs
✅ Widget, preview, vanity URL
✅ Templates
✅ List, create, modify, delete roles
✅ Fetch individual role (getGuildRole)
✅ Gradient role colors (RoleColors, RoleColorStop)
✅ Slash commands (global & guild)
✅ Autocomplete
✅ Modals and components
✅ Interaction responses (including launchActivity type 12)
✅ Follow-up messages
✅ Command localization
✅ Modal components: Label (21), RadioGroup (22), CheckboxGroup (23), Checkbox (24)
✅ Get/modify current user
✅ Guild tag (primary_guild), banner, accent color, flags
✅ Modify current member (nick, avatar, banner, bio)
✅ Create, list, get, delete invites
✅ Community invite role assigning (role_ids)
✅ Target user list management (getInviteTargetUsers, updateInviteTargetUsers, getInviteTargetUsersJobStatus)
✅ Join/leave voice channels (Gateway)
✅ Get voice states via REST (getCurrentUserVoiceState, getUserVoiceState)
✅ Opus audio playback (experimental)
✅ App subscriptions with renewal_sku_ids
✅ Scheduled events
✅ Stage instances
✅ Auto-moderation rules
✅ Application emojis
✅ Role connections (linked roles)
✅ Sticker info (read-only)
✅ Send soundboard sounds
✅ List, create, modify, delete guild soundboard sounds
✅ Soundboard gateway events (SOUNDBOARD_SOUND_CREATE/UPDATE/DELETE)
❌ Guild sticker creation/modification
For unsupported endpoints, use the raw HTTP methods: rawGET, rawPOST, rawPATCH, rawDELETE
Check out the Examples directory for complete, runnable examples:
- PingBot.swift - Simple message responder
- CommandFrameworkBot.swift - Command routing with checks
- SlashBot.swift - Slash command handling
- AutocompleteBot.swift - Autocomplete interactions
- ComponentsExample.swift - Buttons, selects, and embeds
- ViewExample.swift - Persistent interactive views
- CogExample.swift - Modular bot architecture
- FileUploadBot.swift - Sending files
- ThreadsAndScheduledEventsBot.swift - Thread and event handling
- VoiceStdin.swift - Voice playback (experimental)
- LinkedRolesBot.swift - Role connections
- Wiki - Setup guides, concepts, and deployment tips
- Examples - Complete working examples
- Discord Server - Get help and discuss the library
- Changelog - Version history and migration guides
# Build the library
swift build
# Run tests
swift test
# With code coverage (macOS)
swift test --enable-code-coverageCI runs on macOS (Xcode 16.4) and Windows (Swift 6.2). Requires Swift 6.2+ toolchain.
We welcome contributions! Whether it's bug reports, feature requests, or pull requests, we'd love your help making SwiftDisc better.
Before contributing, please:
- Check existing issues and PRs to avoid duplicates
- Open a discussion issue for significant changes before starting work
- Join our Discord server if you have questions
Major release delivering Swift 6 strict-concurrency, typed throws throughout the
REST layer, 32 new event callbacks, full Guild model, critical bug fixes, and
high-impact DX improvements (message.reply(), sendDM(), typed slash accessors,
filtered event streams, background cache eviction). See CHANGELOG.md.
- Full Components V2 fluent builders (MediaGallery, Section, Container, Separator)
- Guild sticker creation and modification
- Enhanced and stable voice support
- Expanded test coverage across REST and Gateway layers
Have ideas? Open an issue or join the discussion on Discord!
If you need help or have questions:
- Check the Wiki for guides and documentation
- Browse Examples for code samples
- Join our Discord server for live help
- Search existing issues for similar questions
SwiftDisc is released under the MIT License. See LICENSE for details.
Built with ❤️ using Swift