Skip to content

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.

iOS 26+ macOS 26+ visionOS 26+ GitHub
.package(url: "https://github.com/Swiftly-Developed/SwiftlyFeedbackKit-Swift-SDK.git", from: "1.3.0")

Installation

Add the FeedbackKit Swift SDK to your project using Swift Package Manager.

Xcode

  1. 1 Open your project in Xcode
  2. 2 Go to File → Add Package Dependencies
  3. 3 Enter the repository URL: https://github.com/Swiftly-Developed/SwiftlyFeedbackKit-Swift-SDK.git
  4. 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. 5 Add the SwiftlyFeedbackKit product. Trust the string-catalog build plugin when Xcode asks. CI passes -skipPackagePluginValidation.

Package.swift

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.

MyApp.swift
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.

MyApp.swift
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

config.swift
// 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.

ContentView.swift
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.

ContentView.swift
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.

FeedbackRow.swift
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].
api-usage.swift
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.

user.swift
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

theme.swift
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.

events.swift
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

error-handling.swift
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
    }
}