Swift SDK
The FeedbackKit Swift SDK adds a feature-request board, a submit form and voting to iOS, macOS and visionOS apps, with ready-made SwiftUI views such as FeedbackListView and SubmitFeedbackView. Add the package with Swift Package Manager, configure it with your project API key, and present the views.
Installation
Add the FeedbackKit Swift SDK to your project using Swift Package Manager.
Xcode
- 1 Open your project in Xcode
- 2 Go to File → Add Package Dependencies
-
3
Enter the repository URL:
https://github.com/Swiftly-Developed/SwiftlyFeedbackKit-Swift-SDK.git - 4 Select version 1.3.0 (Up to Next Major). Code written for 1.1.1 or earlier needs the migration notes, or pin Up to Next Minor from 1.1.1.
-
5
Add the SwiftlyFeedbackKit product. Trust the string-catalog build plugin when Xcode asks. CI passes
-skipPackagePluginValidation.
Package.swift
dependencies: [
.package(
url: "https://github.com/Swiftly-Developed/SwiftlyFeedbackKit-Swift-SDK.git",
from: "1.3.0"
)
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "SwiftlyFeedbackKit", package: "SwiftlyFeedbackKit")
]
)
]
Quick Start
Initialize the SDK with your project API key in your app's entry point and start using the pre-built SwiftUI views.
import SwiftUI
import SwiftlyFeedbackKit
@main
struct MyApp: App {
init() {
// Configure SDK per build environment
#if DEBUG
SwiftlyFeedback.configure(
environment: .development,
key: "your-dev-api-key"
)
#elseif TESTFLIGHT
SwiftlyFeedback.configure(
environment: .testflight,
key: "your-staging-api-key"
)
#else
SwiftlyFeedback.configure(
environment: .production,
key: "your-prod-api-key"
)
#endif
}
var body: some Scene {
WindowGroup {
FeedbackListView()
}
}
}
Auto-Detection Configuration
Alternatively, use configureAuto to automatically detect the build environment and select the appropriate API key.
import SwiftlyFeedbackKit
@main
struct MyApp: App {
init() {
// Auto-detect environment and use appropriate key
SwiftlyFeedback.configureAuto(keys: EnvironmentAPIKeys(
debug: "your-dev-key", // Development API. Nil uses the testflight key.
testflight: "your-staging-key", // Staging server
production: "your-prod-key" // App Store
))
}
var body: some Scene {
WindowGroup {
FeedbackListView()
}
}
}
Configuration
configure(environment:key:) picks the server. configureAuto(keys:) picks the key from the build type. A DEBUG build talks to the development API. Localhost is only .local.
| Environment | Server |
|---|---|
| .local | http://localhost:8080/api/v1 |
| .development | https://api.dev.getfeedbackkit.com/api/v1 |
| .testflight | https://api.testflight.getfeedbackkit.com/api/v1 |
| .production | https://api.prod.getfeedbackkit.com/api/v1 |
Settings live on SwiftlyFeedback.configuration. Set them on the main actor.
| Option | Type | Default | Description |
|---|---|---|---|
| loggingEnabled | Bool | true | OSLog for method, path, and status. Never the API key or bodies. |
| userEmail | String? | — | Preset vote-notification email. Skips the vote dialog. |
| showVoteEmailField | Bool | true | Show email field when voting |
| voteNotificationDefaultOptIn | Bool | false | Default value for notification opt-in |
| allowFeedbackSubmission | Bool | true | Allow users to submit feedback |
| feedbackSubmissionDisabledMessage | String | Default message | Message shown when submission disabled |
| enableAutomaticViewTracking | Bool | true | Track feedback_list, feedback_detail, and submit_feedback |
| translationEnabled | Bool | true | On-device translation. Absent on visionOS. |
| showMailingListOptIn | Bool | true | Mailing-list toggle on submit and in the vote dialog |
| mailingListDefaultOptIn | Bool | false | Initial state of that toggle |
Configuration Example
// Main-actor settings. Call from App.init or a SwiftUI view.
SwiftlyFeedback.configuration.loggingEnabled = false
SwiftlyFeedback.configuration.userEmail = "user@example.com"
SwiftlyFeedback.configuration.allowFeedbackSubmission = false
SwiftlyFeedback.configuration.feedbackSubmissionDisabledMessage = "Coming soon!"
SwiftlyFeedback.configuration.showVoteEmailField = false
SwiftlyFeedback.configuration.voteNotificationDefaultOptIn = true
config, theme, and shared still compile. They are deprecated shims for configuration, appearance, and current. New code uses the new names. current stays nil until configure finishes resolving a user id.
Views
The SDK provides pre-built SwiftUI views that can be easily integrated into your app.
FeedbackListView
Searchable list. Sort by votes (default), newest, oldest, or comment count. The sort is stored in UserDefaults. Status and category tokens narrow the search. Pull to refresh on iOS. Command-R refreshes on macOS.
import SwiftUI
import SwiftlyFeedbackKit
struct ContentView: View {
var body: some View {
// Use default instance
FeedbackListView()
// Or pass a custom instance
FeedbackListView(swiftlyFeedback: SwiftlyFeedback.current)
}
}
SubmitFeedbackView
Form for submitting new feedback with title, description, and category selection.
import SwiftUI
import SwiftlyFeedbackKit
struct ContentView: View {
@State private var showingSubmitForm = false
var body: some View {
Button("Submit Feedback") {
showingSubmitForm = true
}
.sheet(isPresented: $showingSubmitForm) {
SubmitFeedbackView {
showingSubmitForm = false
}
}
}
}
FeedbackDetailView
Detailed view showing feedback information, voting, and comments.
import SwiftUI
import SwiftlyFeedbackKit
struct FeedbackRow: View {
let feedback: Feedback
var body: some View {
NavigationLink {
FeedbackDetailView(feedback: feedback)
} label: {
Text(feedback.title)
}
}
}
Vote dialog
Voting with no preset email presents a dialog. The view type is internal, so you do not embed it. The list and the detail screen present the same dialog. It collects an optional email, a status-change notification toggle, and a mailing-list opt-in. It stays closed when configuration.userEmail is already set, and when configuration.showVoteEmailField is false.
API Reference
Instance methods run on SwiftlyFeedback.current, which is nil until configure finishes. view is static.
| Method | Description |
|---|---|
| getFeedback(status:category:) async throws -> [Feedback] | Fetch feedback. Both filters are optional. |
| submitFeedback(...) async throws -> Feedback | Submit new feedback |
| vote(for:email:notifyStatusChange:) async throws -> VoteResult | Vote for feedback |
| unvote(for:) async throws -> VoteResult | Remove a vote |
| getComments(for:) async throws -> [Comment] | Get feedback comments |
| addComment(to:content:) async throws -> Comment | Add a comment |
| SwiftlyFeedback.view(_:properties:) | Static. Properties are [String: String]. |
import SwiftlyFeedbackKit
guard let client = SwiftlyFeedback.current else { return }
// Get all feedback
let feedbacks = try await client.getFeedback()
// Submit feedback
let feedback = try await client.submitFeedback(
title: "Add dark mode",
description: "Please add dark mode support",
category: .featureRequest,
email: "user@example.com"
)
// Vote for feedback
let result = try await client.vote(
for: feedback.id,
email: "user@example.com",
notifyStatusChange: true
)
// Remove a vote
try await client.unvote(for: feedback.id)
// Get comments
let comments = try await client.getComments(for: feedback.id)
// Add comment
let comment = try await client.addComment(
to: feedback.id,
content: "Great idea!"
)
// Track an event. view is static. Property values are strings.
SwiftlyFeedback.view("onboarding_completed", properties: [
"steps": "5"
])
Models
Feedback
| Property | Type | Description |
|---|---|---|
| id | UUID | Unique identifier |
| title | String | Feedback title |
| description | String | Detailed description |
| status | FeedbackStatus | Current status |
| category | FeedbackCategory | Feedback category |
| voteCount | Int | Total vote count |
| hasVoted | Bool | User voted status |
| commentCount | Int | Total comment count |
| userId | String | SDK user id of the creator |
| userEmail | String? | Creator email, when provided |
| createdAt | Date? | Creation timestamp |
| updatedAt | Date? | Last update timestamp |
| rejectionReason | String? | Present when status is rejected |
| isHiddenOverLimit | Bool? | Free-plan row hidden from the public list. The creator can still open it. |
| mergedIntoId | UUID? | Set when this item was merged into another |
Enums
FeedbackStatus
| Case | Raw Value |
|---|---|
| pending | "pending" |
| approved | "approved" |
| inProgress | "in_progress" |
| testflight | "testflight" |
| completed | "completed" |
| rejected | "rejected" |
FeedbackCategory
| Case | Raw Value |
|---|---|
| featureRequest | "feature_request" |
| bugReport | "bug_report" |
| improvement | "improvement" |
| other | "other" |
Comment
| Property | Type | Description |
|---|---|---|
| id | UUID | Unique identifier |
| content | String | Comment text |
| userId | String | User identifier |
| isAdmin | Bool | Admin comment flag |
| createdAt | Date | Creation timestamp |
VoteResult
| Property | Type | Description |
|---|---|---|
| feedbackId | UUID | Feedback identifier |
| voteCount | Int | Updated vote count |
| hasVoted | Bool | Current vote status |
User ID
The SDK picks an id in this order: a custom id from updateUser(customID:), a Keychain id, an iCloud record name (prefix icloud_, 3 second timeout), then a generated UUID (prefix local_). A custom id updates the live client. updateUser(email:) is a deprecated no-op.
SwiftlyFeedback.updateUser(customID: "user_12345")
SwiftlyFeedback.updateUser(payment: .monthly(9.99))
UserIdentifier.clearUserId()
Theming
Set colors on SwiftlyFeedback.appearance on the main actor. Buttons and highlights read primaryColor. There is no accent color property. .default resolves to the system accent color.
ThemeColor
| Case | Description |
|---|---|
| .default | Use system default color |
| .color(Color) | Use specific SwiftUI Color |
| .adaptive(light:dark:) | Different colors for light and dark mode. .set(light:dark:) is the same value. |
Theme Customization
import SwiftUI
import SwiftlyFeedbackKit
// Primary color for buttons and highlights
SwiftlyFeedback.appearance.primaryColor = .color(.purple)
// Light and dark values
SwiftlyFeedback.appearance.primaryColor = .adaptive(
light: .blue,
dark: .cyan
)
// Status and category colors are ThemeColor, not Color
SwiftlyFeedback.appearance.statusColors.pending = .color(.gray)
SwiftlyFeedback.appearance.statusColors.testflight = .color(.cyan)
SwiftlyFeedback.appearance.statusColors.completed = .color(.green)
SwiftlyFeedback.appearance.categoryColors.bugReport = .color(.red)
Events
Opening a list, detail, or submit screen records feedback_list, feedback_detail, or submit_feedback when enableAutomaticViewTracking is true. view is a static method. Property values are strings.
SwiftlyFeedback.view("checkout_completed", properties: [
"plan": "pro"
])
SwiftlyFeedback.configuration.enableAutomaticViewTracking = false
Plan limits
The server enforces these numbers. The SDK does not. The Free feedback cap saves the row, marks isHiddenOverLimit, and omits it from list reads. That write is HTTP 2xx, not 402.
| Plan | Projects | Feedback | Members | Integrations |
|---|---|---|---|---|
| Free | 1 | 10 visible | No | No |
| Pro | 2 | Unlimited | No | Yes |
| Team | Unlimited | Unlimited | Yes | Yes |
Platforms
Package.swift declares iOS 26, macOS 26, and visionOS 26. It does not declare watchOS or tvOS. The package compiles in Swift 6 language mode.
- iOS uses a form for submit and pull-to-refresh on the list. iPhone and iPad use a navigation split view.
- macOS uses a grid submit form, a 400 by 350 point minimum for submit, and Command-Return to submit. Command-R refreshes the list and the detail comments.
- visionOS runs the same list, submit, detail, and vote dialog. On-device translation is absent. There is no disabled control in its place.
Error Handling
The SDK uses the SwiftlyFeedbackError enum for error handling.
| Error | Description |
|---|---|
| invalidResponse | Invalid server response |
| badRequest | Invalid request parameters |
| unauthorized | Authentication failed |
| invalidApiKey | Invalid or missing API key |
| notFound | Resource not found |
| conflict | Conflicting operation (e.g., duplicate vote) |
| serverError | Internal server error |
| networkError | Network connectivity error |
| decodingError | Failed to decode response |
| feedbackLimitReached | HTTP 402. The Free feedback cap does not raise this. That cap hides the extra row. |
| forbidden | HTTP 403 from shared middleware (role, access, or suspension) |
| rateLimited | HTTP 429 |
Error Handling Example
import SwiftlyFeedbackKit
do {
let feedbacks = try await SwiftlyFeedback.current?.getFeedback()
} catch let error as SwiftlyFeedbackError {
switch error {
case .invalidApiKey:
break
case .feedbackLimitReached:
break
default:
break
}
}
Guides
How to Add a Feedback Board to a SwiftUI App
Add a feedback board to a SwiftUI app with FeedbackKit: Swift Package Manager, the API key, list, submit and detail views, voting, MRR and status emails.
17 min read
What Is a Feedback SDK? A Practical Guide for App Teams
A feedback SDK lets users send, vote on and track feature requests inside your app. How it differs from surveys, support desks and analytics.
9 min read
How to Choose an In-App Feature Request Tool for Your iOS App
A buyer's guide for iOS developers: the eight questions to ask before you pick a feature request tool for your app.
4 min read
In-App Feedback Tools for iOS Apps, Compared
A fair roundup of in-app feedback tools for iOS: native feature voting SDKs, bug reporting suites, chat inboxes and web boards in a webview, with prices.
7 min read