Feedback SDK / iOS
iOS Feedback SDK Integration Guide
Let users submit ideas, bugs, feedback, and questions; vote on public items; and identify themselves or stay anonymous. Feedback reuses the apiKey and baseURL from GrowthCat.initialize.
Platform
Native iOS apps using the GrowthCat SDK.
Use Case
Collect private feedback and public idea votes with user identity, anonymous identity, and app metadata.
Visibility
Only idea items are public. Bugs, feedback, and questions stay private in the dashboard.
Overview
The Feedback SDK provides a native board-first feedback experience. Users can browse and vote on public ideas, open the compose sheet from the floating + button, submit private bug reports and questions, and keep using an anonymous identity until your app identifies them.
Note on visibility
The backend only ever makes idea items public. Bug, feedback, and question submissions are always private, never appear on the public board, and never count toward votes from other users. Plan your UI copy accordingly: a submitted bug report will not show up for others to upvote.
Configuring The Feedback SDK
Import GrowthCat and call GrowthCat.initialize once before using feedback. GrowthCatFeedback.configure is only for feedback-specific options like user identity, theme, strings, and metadata filtering.
import GrowthCat
GrowthCat.initialize(
apiKey: "gc_live_your_key_here",
logsEnabled: growthCatOptions.logsEnabled,
baseURL: baseURL
)
GrowthCatFeedback.configure(
user: FeedbackUser(
id: "customer_123",
email: "jane@example.com",
name: "Jane"
)
)Omit user to start anonymous. The SDK creates and persists an anonymous ID for you.
Identifying Feedback Users
Call identify once the app knows who the visitor is. Future submissions and votes use userId instead of the anonymous ID.
GrowthCatFeedback.identify(
FeedbackUser(id: "customer_123", email: "jane@example.com", name: "Jane")
)Call clearUser on sign-out so the next signed-in user is not linked to the previous identity. Future submissions and votes fall back to the already-persisted anonymous ID.
GrowthCatFeedback.clearUser()Anonymous Feedback Users
If no user is configured, the SDK generates an anonymous ID, such as anon_6a4d72, and persists it in the Keychain so it survives app relaunches. You never need to manage it manually.
let anonymousId = GrowthCatFeedback.shared.anonymousIdSubmitting Feedback Programmatically
let result = try await GrowthCatFeedback.submit(
FeedbackSubmission(
type: .bug,
title: "Export button crashes",
body: "When I tap Export from the reports screen the app freezes.", // optional
metadata: ["screen": "ReportsView"]
)
)
print(result.itemId, result.status, result.voteCount)FeedbackType is one of .idea, .bug, .feedback, or .question. FeedbackSubmission.body is optional and omitted from the request when empty. FeedbackSubmitResult.status is one of .new, .triage, .todo, .inProgress, .done, or .closed.
Presenting The Native Feedback Sheet
presentFeedbackSheet opens the board as the first screen, with the compose sheet reachable from its floating + button.
GrowthCatFeedback.shared.presentFeedbackSheet(
from: viewController,
preselectedType: .bug, // optional — pre-applies a type filter; nil shows everything
metadata: ["screen": "ReportsView"]
)Board first screen
- A horizontally scrolling filter row with All plus one chip per FeedbackType filters the list client-side.
- Each row shows an upvote control, vote count, type tag, title, and optional body preview.
- Tapping the vote control calls vote or unvote optimistically, then reconciles with the server voteCount and hasVoted values.
- Pull-to-refresh, an empty state, and a load-error state with retry are built in.
- The floating + button opens compose, pre-filled with the active filter or .feedback when All is selected.
Compose sheet
- Includes type selector, title field, body field, optional email field for anonymous users, submit button, and success and error states.
- Returning from compose automatically refreshes the board.
- Both screens support Dark Mode through light and dark color pairs resolved against the system appearance.
Customizing Feedback Appearance And Text
Configure colors and strings at setup time so the UI matches your app and can be localized. Each color is a light/dark pair so it adapts automatically.
GrowthCatFeedback.configure(
theme: GrowthCatFeedbackTheme(
accent: FeedbackHexPair(light: "#0A84FF", dark: "#409CFF"),
background: FeedbackHexPair(light: "#F2F2F7", dark: "#000000"),
card: FeedbackHexPair(light: "#FFFFFF", dark: "#1C1C1E"),
primaryText: FeedbackHexPair(light: "#111111", dark: "#F5F5F5"),
secondaryText: FeedbackHexPair(light: "#6B6B70", dark: "#9B9BA1"),
border: FeedbackHexPair(light: "#DDDDDD", dark: "#38383A")
),
strings: GrowthCatFeedbackStrings(
title: "Send Feedback",
cancel: "Close",
submit: "Submit request",
submitting: "Submitting",
successTitle: "Request sent",
typeLabel: "Category",
titleLabel: "Summary",
titlePlaceholder: "What should we add?",
detailsLabel: "Details",
detailsPlaceholder: "Tell us what you would like to see",
emailLabel: "Email (optional)",
emailPlaceholder: "you@example.com",
errorFallback: "Something went wrong. Please try again.",
typeLabels: [
.idea: "Request",
.bug: "Bug",
.feedback: "Feedback",
.question: "Question"
],
boardTitle: "Feedback",
allFilterLabel: "All",
emptyBoardTitle: "No feedback yet",
emptyBoardMessage: "Be the first to share an idea, report a bug, or ask a question.",
loadErrorFallback: "Couldn't load feedback. Please try again.",
retry: "Retry"
)
)Use #RRGGBB hex strings for both light and dark. FeedbackHexPair(_:) accepts a single hex if you do not want to vary by appearance, and the single-hex convenience initializer remains available for existing call sites. Any omitted theme or string value falls back to the SDK default.
Feedback Voting
Voting is for public items only. One vote per user is enforced server-side; treat duplicate-vote responses as normal and refresh from the returned voteCount. The native board does this automatically.
let voteResult = try await GrowthCatFeedback.vote(itemId: "fit_123")
let unvoteResult = try await GrowthCatFeedback.unvote(itemId: "fit_123")To fetch the board yourself, optionally filtered by type:
let allItems = try await GrowthCatFeedback.fetchBoard()
let bugsOnly = try await GrowthCatFeedback.fetchBoard(type: .bug)Feedback Automatic Metadata
Every submission and vote automatically includes app and device context.
{
"platform": "ios",
"app_version": "1.2.0",
"build_number": "42",
"os_version": "26.0",
"device_model": "iPhone17,2",
"locale": "en-US"
}- app_version comes from CFBundleShortVersionString, and build_number comes from CFBundleVersion.
- No raw screenshots, logs, or IP addresses are ever collected by the SDK.
- You can disable individual automatic fields with disabledAutomaticMetadataKeys.
- You can add or override metadata globally with setMetadata, or per submission with FeedbackSubmission(metadata:).
GrowthCatFeedback.configure(
disabledAutomaticMetadataKeys: ["device_model"]
)GrowthCatFeedback.setMetadata(["plan": "pro"])Feedback Error Handling
public enum GrowthCatFeedbackError: Error {
case notConfigured
case invalidPayload(message: String?)
case networkError(message: String?)
case boardNotFound
case feedbackDisabled
case planLimitReached
case rateLimited(retryAfter: TimeInterval?)
case serverError(statusCode: Int, message: String?)
}do {
_ = try await GrowthCatFeedback.submit(
FeedbackSubmission(type: .bug, title: "Export button crashes", body: "It freezes.")
)
} catch GrowthCatFeedbackError.planLimitReached {
print("Feedback is temporarily unavailable.")
} catch {
print(error.localizedDescription)
}Feedback API Reference
public enum GrowthCatFeedback {
public static var isConfigured: Bool
public static var shared: GrowthCatFeedbackClient
public static func configure(
user: FeedbackUser? = nil,
theme: GrowthCatFeedbackTheme = GrowthCatFeedbackTheme(),
strings: GrowthCatFeedbackStrings = GrowthCatFeedbackStrings(),
disabledAutomaticMetadataKeys: Set<String> = []
)
public static func identify(_ user: FeedbackUser)
public static func clearUser()
public static func setMetadata(_ metadata: [String: String])
public static func submit(_ submission: FeedbackSubmission) async throws -> FeedbackSubmitResult
public static func vote(itemId: String) async throws -> FeedbackVoteResult
public static func unvote(itemId: String) async throws -> FeedbackVoteResult
public static func fetchBoard(type: FeedbackType? = nil) async throws -> [FeedbackBoardItem]
}
public struct GrowthCatFeedbackClient {
public var isConfigured: Bool
public var anonymousId: String?
public var currentUser: FeedbackUser?
public var theme: GrowthCatFeedbackTheme
public var strings: GrowthCatFeedbackStrings
public func identify(_ user: FeedbackUser)
public func clearUser() // Call on sign-out — reverts to the anonymous ID.
public func setMetadata(_ metadata: [String: String])
public func submit(_ submission: FeedbackSubmission) async throws -> FeedbackSubmitResult
public func vote(itemId: String) async throws -> FeedbackVoteResult
public func unvote(itemId: String) async throws -> FeedbackVoteResult
public func fetchBoard(type: FeedbackType? = nil) async throws -> [FeedbackBoardItem]
// iOS 16+ — presents the board first, with compose reachable via its "+" button.
@MainActor
public func presentFeedbackSheet(
from viewController: UIViewController,
preselectedType: FeedbackType? = nil,
metadata: [String: String]? = nil
)
}
public enum FeedbackType: String { case idea, bug, feedback, question }
public struct FeedbackHexPair {
public let light: String
public let dark: String
}
public struct GrowthCatFeedbackTheme {
public let accent: FeedbackHexPair
public let background: FeedbackHexPair
public let card: FeedbackHexPair
public let primaryText: FeedbackHexPair
public let secondaryText: FeedbackHexPair
public let border: FeedbackHexPair
}
public struct GrowthCatFeedbackStrings {
public let title: String
public let cancel: String
public let submit: String
public let submitting: String
public let successTitle: String
public let typeLabel: String
public let titleLabel: String
public let titlePlaceholder: String
public let detailsLabel: String
public let detailsPlaceholder: String
public let emailLabel: String
public let emailPlaceholder: String
public let errorFallback: String
public let typeLabels: [FeedbackType: String]
public let boardTitle: String
public let allFilterLabel: String
public let emptyBoardTitle: String
public let emptyBoardMessage: String
public let loadErrorFallback: String
public let retry: String
}
public struct FeedbackUser {
public let id: String
public let email: String?
public let name: String?
}
public struct FeedbackSubmission {
public let type: FeedbackType
public let title: String
public let body: String // Optional at init; defaults to "" and is omitted when empty.
public let metadata: [String: String]?
}
// The public-facing item shape — same fields whether it comes from submit,
// board listing, or vote/unvote. There is no visibility field: the backend
// only ever exposes idea items publicly, so anything you can see here is
// implicitly public.
public struct FeedbackSubmitResult {
public let itemId: String
public let type: FeedbackType
public let title: String
public let body: String?
public let status: FeedbackItemStatus // .new, .triage, .todo, .inProgress, .done, .closed
public let voteCount: Int
public let commentCount: Int
public let isPinned: Bool
public let hasVoted: Bool
public let createdAt: Date
public let updatedAt: Date
}
public struct FeedbackVoteResult {
public let itemId: String
public let hasVoted: Bool
public let voteCount: Int
}
public struct FeedbackBoardItem: Identifiable {
public let itemId: String
public let type: FeedbackType
public let title: String
public let body: String?
public let status: FeedbackItemStatus
public let voteCount: Int
public let commentCount: Int
public let isPinned: Bool
public let hasVoted: Bool
public let createdAt: Date
public let updatedAt: Date
public var id: String { itemId }
}