← All posts Guides

Add in-app feedback to an iOS app in two calls

One Swift package, one initialize call, one SwiftUI view. The SwiftPM rule the beta needs, and the three places iOS integrations go wrong.

· · 8 min read
A dark code editor window with four highlighted statements, an orange arrow pointing right to a phone where a fedo feedback list has opened with vote arrows and an add button

Adding a feedback board to an iOS app takes one Swift package and two SDK calls. The view renders the whole thing — list, details, editor, voting, comments — inside your own NavigationStack, and there is no backend to run.

As of 0.4.0-beta.1 (September 2026), the complete integration is this:

MyApp.swift
Fedo.initialize(apiKey: "your-api-key-here")
FeedbackLink.swift
NavigationLink("Feedback") {
    FedoFeedbackView()
}

Everything below is detail on those two blocks, plus the optional identity calls and the three mistakes that actually cost people an afternoon. The first one is a package resolution rule, and it will stop you before you write any Swift at all.

Key takeaways#

  • The package is https://github.com/getfedo/fedo-ios, and you must pin it to the Exact Version 0.4.0-beta.1. SwiftPM skips pre-releases under "Up to Next Major", so the default dependency rule resolves to nothing.
  • iOS is in beta. The Android SDK is a stable 0.4.0 on Maven Central; iOS reached feature parity with it on 17 September 2026 and is still tagged as a pre-release.
  • Requires iOS 16+, Swift 6, and Xcode 26+. The module you import is FedoKit; the entry point is Fedo.
  • Fedo.initialize must run before any other SDK method — every other call reads state it sets up.
  • FedoFeedbackView is a plain SwiftUI View, not a presenter. It needs a NavigationStack above it, and when you show it in a .sheet you have to supply that stack yourself.
  • If you never call setUserID, the SDK creates a cached anonymous user and everything still works.

1. How do you add the Swift package?#

In Xcode: File → Add Package Dependencies, paste https://github.com/getfedo/fedo-ios, and set the dependency rule to Exact Version 0.4.0-beta.1.

Or in Package.swift:

Package.swift
.package(url: "https://github.com/getfedo/fedo-ios", exact: "0.4.0-beta.1")

This is the first mistake. Xcode defaults every new package to "Up to Next Major Version", and SwiftPM does not consider pre-release versions for a range-based rule — semver treats 0.4.0-beta.1 as lower precedence than 0.4.0, and Apple's resolver leaves pre-releases out of ranges unless you name one exactly. With the default rule, Xcode reports that no version satisfies the requirement even though the tag is right there in the repo. Switch the rule to Exact Version. See Apple's notes on adding package dependencies.

This also means SDK updates are a manual bump for as long as iOS stays in beta. Watch the iOS changelog rather than waiting for Xcode to offer you an update.

2. When and where do you initialize the SDK?#

MyApp.swift
import SwiftUI
import FedoKit

@main
struct MyApp: App {
    init() {
        Fedo.initialize(apiKey: "your-api-key-here")
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

The App initializer is the natural home: it runs once, before any scene is built, which is exactly the ordering the SDK wants. A UIApplicationDelegate's application(_:didFinishLaunchingWithOptions:) works the same way if you still have one.

This is the second mistake. Fedo.initialize must be called before any other SDK method. It is easier to get wrong in SwiftUI than it looks, because a @StateObject on your root view, or a dependency container built as a property wrapper, can construct before you think it does. Initialize in App.init(), then everything else.

The API key identifies your board. Ship a different key to point the same build at a different board — that is how you keep a staging board separate from production.

To change SDK behaviour, pass a config:

MyApp.swift
Fedo.initialize(
    apiKey: "your-api-key",
    config: FedoConfig(
        networkTimeout: 15,
        logLevel: .debug
    )
)

config is optional and defaults to FedoConfig.default; config: .init(...) is the usual shorthand. networkTimeout is in seconds, defaults to 10, and caps every SDK request. logLevel defaults to .none; .debug writes verbose output and is the fastest way to see whether a failing call is yours or the SDK's. Ship .none.

Configuration is read once, at initialization, and stays fixed for the process. If you want it to vary, compute the value at startup from a build configuration or a remote flag and pass the result in — do not re-initialize. Full table on the configuration page.

3. How do you tell the SDK who the user is?#

All of the identity methods are optional, and all of them must come after initialize:

AuthStore.swift
Fedo.setUserID("user-abc-123")
Fedo.setUserDisplayName("Jane Doe")
Fedo.setUserEmail("jane@example.com")
Fedo.setUserProperty("plan", value: "enterprise")

setUserID is the one that matters. Call it when you actually know who the user is — at registration and at login.

Do not call setUserID for anonymous visitors. If you skip it entirely, the SDK creates its own anonymous user with a random ID and persists it, so the same person keeps the same identity across sessions. Inventing a placeholder ID for signed-out users defeats that and produces a new "user" every time your placeholder changes.

setUserProperty(_:value:) attaches arbitrary key-value metadata — plan tier, team, beta cohort. Note the external label on the second parameter; the Android signature is positional and the Swift one is not:

swift
Fedo.setUserProperty("tier", value: "gold")
Fedo.setUserProperty("tier", value: "silver") // overrides the line before

You do not need to attach device metadata yourself. The SDK collects it during initialization and refreshes it each session: _platform, _manufacturer, _model, _device, _osVersion, _sdkVersion, _locale, _timeZone, _appVersionName, and _appBuild. They arrive as properties prefixed with an underscore and cannot be disabled. Full table in the user management docs.

There is a Fedo.logout() too. It clears the cached session and identity, then creates a fresh anonymous user. Session tokens live in the keychain and are refreshed by the SDK, so there is nothing for you to store or rotate.

4. How do you show the feedback board?#

SettingsView.swift
import SwiftUI
import FedoKit

struct SettingsView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Feedback") {
                    FedoFeedbackView()
                }
            }
        }
    }
}

What you get is a complete board with its own internal navigation. The list shows feedback with voting and a way in to create more. The details screen has the full description, threaded comments, voting, replies, and edit or delete of the user's own feedback when its status allows it. The editor creates and edits, pre-populating fields and validating before submit. Loading, error, and empty states are handled internally — there is no extra UI to build.

This is the third mistake. FedoFeedbackView is a View, not a presenter, and it pushes its own detail and editor screens. Dropped into a hierarchy with no NavigationStack above it, the pushes go nowhere and the board looks frozen on its first screen. A NavigationLink from an existing stack is fine. A sheet is not, unless you add the stack:

swift
.sheet(isPresented: $showFeedback) {
    NavigationStack {
        FedoFeedbackView()
    }
}

There is no onDismiss parameter here, which is the visible difference from the Android composable — on iOS the surrounding NavigationStack or sheet owns dismissal, so the system back button and the sheet's own swipe-down already do the right thing.

A naming trap in the docs. The quickstart currently writes the view as FedoFeedbacksView(). The type is FedoFeedbackView, singular, as the SDK reference and the repository README have it. If you copied the quickstart and the compiler says it cannot find the type in scope, that is why.

The submit-only sheet#

If you do not want a whole board — a bug report field buried in settings, say — there is a view modifier that presents just the create form:

swift
Button("Send feedback") { showFeedbackSheet = true }
    .presentFedoCreateFeedback(isPresented: $showFeedbackSheet)

No navigation setup required for this one. It is title and description, submitted straight to your board, and it is the right choice when you want reports in and do not want users browsing each other's requests yet.

What the SDK does not do yet#

Worth knowing before you ship rather than after:

  • No UI configuration on iOS. FedoFeedbackView and .presentFedoCreateFeedback follow the SDK's own styling and the system navigation bar. The slot and style APIs described on the UI configuration page are Android-only today.
  • No retry, no offline support. A failed call is skipped, not retried. If setUserID fails, you call it again. Offline support is planned, not present.
  • Beta, and pinned. Exact-version pinning means no automatic patch pickup. Check the changelog when you next touch the project.
  • Android and iOS. Flutter is in development. The dashboard is shared, so nothing you configure now is iOS-specific.
  • Anonymous-to-authenticated migration is one-way. Covered in how anonymous feedback migrates to a real account — the short version is that calling setUserID on an anonymous user moves their feedback, comments, and votes onto the authenticated account and then deletes the anonymous one.

The whole thing#

swift
// 1. Package.swift -- Exact Version, not a range
.package(url: "https://github.com/getfedo/fedo-ios", exact: "0.4.0-beta.1")

// 2. App.init -- before any other SDK call
Fedo.initialize(apiKey: "your-api-key-here")

// 3. after login (optional)
Fedo.setUserID(user.id)
Fedo.setUserEmail(user.email)

// 4. anywhere inside a NavigationStack
NavigationLink("Feedback") { FedoFeedbackView() }

Two calls, plus the package. The getting started guide has the same material in reference form, the example app is a running version of it, and the Android walkthrough is the same post for the other platform.

TopicsiOSSwiftSwiftUISDKIntegration

Add it to your app.

One dependency and two calls. The free plan covers one app, with no card.

build.gradle.kts
implementation("com.getfedo:sdk-android:0.4.0")