Binary Swift Package for realtime in-app toast delivery on iOS.
ToastKit gives iOS teams a single client surface for:
- Realtime foreground toast delivery over WebSocket
- Topic-based routing for user and org-scoped channels
- SwiftUI overlay mounting with one root-level modifier
- APNs-aware client state hooks for fallback-oriented integrations
- Local preview and delivery-path simulation helpers for QA
This repository is the public distribution layer for the iOS SDK. It ships ToastKit as a binary Swift Package backed by a GitHub Release XCFramework.
- Obtain your ToastKit
appKeyand WebSocket endpoint from your deployment environment. - Add
https://github.com/bairisland/ToastKit-iOS.gitto your app with Swift Package Manager. - Configure
ToastKitduring app startup using a backend-issued JWT provider. - Mount
.toastKitOverlay()once near the root of your SwiftUI app. - Subscribe to the user and org topics your session is authorized to receive.
| Component | Version |
|---|---|
| iOS | 17.0+ |
| Xcode | 16+ |
| Distribution | Swift Package Manager |
- Open your project in Xcode.
- Go to
File > Add Package Dependencies... - Search for
https://github.com/bairisland/ToastKit-iOS.git - Select a version rule starting from
0.1.0 - Add the
ToastKitproduct to your application target
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "YourApp",
platforms: [
.iOS(.v17)
],
dependencies: [
.package(url: "https://github.com/bairisland/ToastKit-iOS.git", from: "0.1.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "ToastKit", package: "ToastKit-iOS")
]
)
]
)Use these exact names in consuming code:
- Repository URL:
https://github.com/bairisland/ToastKit-iOS.git - Package identity:
ToastKit-iOS - Product name:
ToastKit - Import name:
ToastKit
import ToastKitThe example below shows the intended SwiftUI integration shape for a production app.
import SwiftUI
import ToastKit
@main
struct ExampleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
init() {
ToastKit.configure(
appKey: "tk_live_your_app_key",
userToken: {
try await AuthManager.shared.fetchToastKitJWT()
},
options: ToastKitOptions(
webSocketURL: URL(string: "wss://realtime.example.com/v1/realtime"),
organizationID: "org_123",
persistDedupe: true,
fallbackEnabled: true
)
)
ToastKit.subscribe([
"user:user_123"
])
ToastKit.subscribeOrgChannel("ops")
ToastKit.onToast { toast in
Analytics.shared.track(
name: "toast_received",
properties: [
"id": toast.id,
"topic": toast.topic,
"delivery_path": toast.deliveryPath.rawValue
]
)
}
}
var body: some Scene {
WindowGroup {
RootView()
.toastKitOverlay()
}
}
}
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
application.registerForRemoteNotifications()
return true
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
ToastKit.registerAPNsDeviceToken(deviceToken)
}
}Use authenticated mode in production. Your backend should mint a short-lived JWT for the current user session.
ToastKit.configure(
appKey: "tk_live_your_app_key",
userToken: {
try await AuthManager.shared.fetchToastKitJWT()
},
options: ToastKitOptions(
webSocketURL: URL(string: "wss://realtime.example.com/v1/realtime"),
organizationID: "org_123"
)
)Recommended use cases:
- user-specific topic authorization
- org-scoped channel authorization
- short-lived token rotation
- production multi-tenant deployments
Use development mode only for local integration, QA, or controlled preview environments.
ToastKit.configure(
appKey: "tk_dev_your_app_key",
developmentUserID: "user_123",
options: ToastKitOptions(
webSocketURL: URL(string: "wss://realtime.example.com/v1/realtime")
)
)This mode is not a substitute for production auth.
Topic subscription is explicit. The SDK does not infer your routing model.
Recommended topic patterns:
user:<userId>org:<orgId>:<channel>
Examples:
ToastKit.subscribe([
"user:user_123",
"org:org_123:billing",
"org:org_123:ops"
])ToastKit.unsubscribe([
"org:org_123:billing"
])If your app is tenant-bound, configure organizationID and use:
ToastKit.subscribeOrgChannel("ops")subscribeOrgChannel(_:) uses the configured ToastKitOptions.organizationID. If no org ID is configured, the call is ignored.
ToastKit exposes client-side APNs state hooks. Register the device token if your integration needs fallback-aware behavior or client-side QA simulation.
Register from Apple callbacks:
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
ToastKit.registerAPNsDeviceToken(deviceToken)
}Register a token string directly:
ToastKit.registerAPNsDeviceTokenString("0123abcd...")Configure environment:
ToastKit.setAPNsEnvironment(.production)Disable fallback-aware behavior:
ToastKit.setFallbackEnabled(false)Clear local token state:
ToastKit.clearAPNsDeviceToken()Important:
- The SDK stores APNs token state locally for client behavior.
- If your backend requires APNs token registration, perform that in your own application/backend integration.
- Do not assume
registerAPNsDeviceTokenby itself publishes token state upstream.
Toasts render in-app only when the overlay is mounted:
RootView()
.toastKitOverlay()Mount it once, as high in the SwiftUI hierarchy as practical.
ToastKit.configure(...) initializes process-wide client state and begins connection flow. Treat configuration as app startup work, not per-screen work.
ToastKit.onToast(_:) appends a handler and does not currently expose a removal token. Register it once during startup or in a dedicated app-wide coordinator.
The following APIs are intended for preview, QA, and local simulation:
ToastKit.show(_:)ToastKit.previewPublish(_:)
They do not replace a real backend publish path.
| Area | API |
|---|---|
| Configure | configure(appKey:userToken:apnsDeviceTokenProvider:options:) |
| Configure for development | configure(appKey:developmentUserID:apnsDeviceTokenProvider:options:) |
| Connection | connect(), disconnect(), retryConnection() |
| Topic management | subscribe(_:), subscribeOrgChannel(_:), unsubscribe(_:) |
| APNs state | registerAPNsDeviceToken(_:), registerAPNsDeviceTokenString(_:), clearAPNsDeviceToken() |
| APNs behavior | setAPNsEnvironment(_:), setFallbackEnabled(_:) |
| Events | onToast(_:), latestToast |
| Local QA helpers | show(_:), previewPublish(_:) |
| SwiftUI | View.toastKitOverlay() |
| Field | Type | Default | Notes |
|---|---|---|---|
baseURL |
URL |
https://api.toastbus.io |
Reserved for broader integration configuration |
webSocketURL |
URL? |
nil |
Required for realtime WebSocket transport |
organizationID |
String? |
nil |
Used by subscribeOrgChannel(_:) |
persistDedupe |
Bool |
false |
Persists seen IDs and dedupe keys across launches |
fallbackEnabled |
Bool |
true |
Enables fallback-aware behavior |
diagnosticsEnabled |
Bool |
true |
Enables internal diagnostics capture |
reconnectPolicy |
ToastKitReconnectPolicy |
default | Exponential reconnect settings |
| Field | Type | Default |
|---|---|---|
initialDelay |
TimeInterval |
0.5 |
multiplier |
Double |
2.0 |
jitter |
Double |
0.2 |
maxDelay |
TimeInterval |
30 |
maxAttempts |
Int |
5 |
These accessors are @MainActor:
let state = await MainActor.run { ToastKit.connectionState }
let topics = await MainActor.run { ToastKit.subscribedTopics }
let latest = await MainActor.run { ToastKit.latestToast }
let apnsEnv = await MainActor.run { ToastKit.apnsEnvironment }ToastKitToast
idtopictitlebodystyleprioritydurationMillisecondsdeepLinkmetadatadedupeKeycollapseKeycreatedAtdeliveryPath
ToastKitToastStyle
.info.success.warning.error
ToastKitToastPriority
.low.normal.high
ToastKitConnectionState
.idle.requestingSession.connecting.connected.disconnected.reconnecting.failed(String)
ToastKitDeliveryPath
.websocket.apnsFallback.apnsFallbackThenReconnect.localMock.dropped
ToastKit.show(
ToastKitToast(
topic: "user:user_123",
title: "Build finished",
body: "The export completed successfully",
style: .success,
priority: .normal
)
)let path = await MainActor.run {
ToastKit.previewPublish(
ToastKitPublishRequest(
topic: "user:user_123",
toast: ToastKitPublishPayload(
title: "Preview toast",
body: "Testing local delivery behavior"
)
)
)
}- Mint tokens server-side.
- Keep tokens short-lived.
- Scope topic claims narrowly.
- Rotate tokens through your existing auth refresh flow.
- Do not embed production bearer tokens in the application bundle.
- Prefer org-scoped topics such as
org:<orgId>:<channel>. - Configure
organizationIDwhen the app session is tenant-bound. - Use
subscribeOrgChannel(_:)to reduce client-side topic construction errors.
If your publisher sends stable IDs, dedupe keys, or collapse keys, the client can suppress duplicate toasts and replace in-flight toasts with newer state.
If dedupe memory should survive app restarts:
let options = ToastKitOptions(
persistDedupe: true
)ToastKit.debug = trueUse this only in debug builds or controlled troubleshooting sessions.
Check that your target dependency references the correct package identity and product:
.product(name: "ToastKit", package: "ToastKit-iOS")Check the common failure points:
.toastKitOverlay()is not mounted at the app root.- The app is not foregrounded.
- No matching topic subscription exists.
- Your
onToastside effects are registered, but the overlay is missing. - Your token claims do not authorize the requested topics.
Check:
- A device token was registered with
ToastKit. - Fallback was not disabled with
setFallbackEnabled(false). - Your wider integration registered the APNs token wherever your backend expects it.
Register ToastKit.onToast once. Re-registering it on repeated view appearances will stack handlers.
This repository publishes a binary target backed by the GitHub Release asset:
https://github.com/bairisland/ToastKit-iOS/releases/download/0.1.0/ToastKit.xcframework.zip
Current 0.1.0 checksum:
0764e1a4c59ed23a8dfb0fcaed786548d15e152804812a06875d5c52165ce4ed
The public Package.swift is intentionally binary-only and points at that asset.
- Current tag:
0.1.0 - Distribution model: binary Swift Package via GitHub Release asset
- Recommended consumer dependency rule:
from: "0.1.0"