iOS (Swift)
The FeedbackJar iOS SDK is a data layer plus an optional prebuilt board. Use just the data layer if you’re building your own UI, or drop in FeedbackJarBoard for a complete list → detail → new feedback flow with voting and comments already wired up.
- Package: Swift Package Manager
- Min iOS: 15.0
- Repository: github.com/feedbackjar/swift-sdk
Installation
In Xcode: File → Add Package Dependencies, enter the repository URL, and add FeedbackJar to your target.
Or in Package.swift:
dependencies: [
.package(url: "https://github.com/feedbackjar/swift-sdk", from: "1.4.6"),
],
targets: [
.target(name: "MyApp", dependencies: ["FeedbackJar"]),
] Setup
Configure once before use — typically in your SwiftUI App.init:
import FeedbackJar
@main
struct MyApp: App {
init() {
FeedbackJar.configure(widgetId: "your-widget-id")
}
} Get your widget ID from Settings → Widgets in your FeedbackJar project.
Prebuilt board (optional)
Skip building your own screens — FeedbackJarBoard is a complete board built only on SwiftUI, a system framework, not a dependency:
import FeedbackJar
import SwiftUI
struct FeedbackTab: View {
var body: some View {
FeedbackJarBoard()
// or: FeedbackJarBoard(accentColor: .blue, boardId: "board_123")
}
} Gives you list, upvote, detail, comments, and a new-feedback form, built only on the API methods below. Nothing throws; failures render inline.
Driving “New feedback” from your own UI
The board shows its own “New” button and Cancel button by default. If your screen has its own button/toolbar action to start a submission, or its own way to back out, turn either off and drive the sheet with your own binding:
@State private var isPresentingNewFeedback = false
FeedbackJarBoard(
showNewButton: false,
showCancelButton: false,
isPresentingNewFeedback: $isPresentingNewFeedback
)
.toolbar {
Button("New") { isPresentingNewFeedback = true }
} The board flips isPresentingNewFeedback back to false on cancel or a
successful send, so your button and the board’s state stay in sync.
Identify the submitter (optional)
Remember a name/email so later submit, vote, and addComment calls attribute to the same person, and so their name/email are synced to the server against this install’s anonymous id:
FeedbackJar.shared.setIdentity(name: user.name, email: user.email)
let identity = FeedbackJar.shared.getIdentity()
// On logout:
FeedbackJar.shared.clearIdentity() Skip this and submissions stay fully anonymous — setIdentity is optional, not required.
Verified identity (signed-in users)
If your app has its own signed-in users, attribute their votes/comments to a real, verifiable account — consistent across every device they use — instead of the unverified name/email above. Your backend computes the signature:
// Your backend, once the user is authenticated:
// timestamp = Int(Date().timeIntervalSince1970 * 1000)
// signature = HMAC-SHA256(orgSecretKey, "\(userId):\(email):\(timestamp)")
// (org secret key is in the FeedbackJar dashboard)
FeedbackJar.shared.setIdentity(
userId: user.id,
email: user.email,
signature: signatureFromYourBackend,
timestamp: timestampFromYourBackend
) Once set, vote, addComment, submit, and hasVoted all attach to this real user on every device — immediately, and any votes/comments already made anonymously on this device are folded in automatically. timestamp must be the exact value your backend signed, and expires after 7 days — call setIdentity again on each sign-in.
Submit feedback
Each submission automatically includes device metadata (iOS version, device model, screen size, app version, locale).
let result = await FeedbackJar.shared.submit(userText)
switch result {
case .success(let response):
print("Submitted: \(response.postId)")
case .failure(let error):
print("Failed: \(error)")
} A callback variant is available when you are not in an async context:
FeedbackJar.shared.submit(userText) { result in
switch result {
case .success(let response): // show success state
case .failure(let error): // show error state
}
} Pass email/name for a headless submission where identity isn’t already known, and properties for your own key/value context merged into the auto-collected app metadata:
await FeedbackJar.shared.submit(userText, properties: ["plan": "pro"]) List feedback
Fetch the public feedback feed with optional pagination:
let result = await FeedbackJar.shared.listFeedback(limit: 20)
if case .success(let page) = result {
for post in page.posts {
print("\(post.title) — \(post.upvotes) upvotes")
}
} Pass boardId to filter by board, or cursor for pagination.
FeedbackJar.shared.getPost(postId) fetches a single public post the same way.
Voting
Guest upvoting as this install’s anonymous guest — idempotent, and gated by the project’s Allow guest votes setting (WidgetConfig.allowVotes, see Get widget config):
await FeedbackJar.shared.vote(postId: postId) // upvote
await FeedbackJar.shared.unvote(postId: postId) // remove the upvote
let state = await FeedbackJar.shared.voteState(postId: postId) // current count + hasVoted Comments
Anonymous, two-level threads — gated by Allow guest comments (WidgetConfig.allowComments):
let comments = await FeedbackJar.shared.listComments(postId: postId, limit: 20)
// Reply to a post, or to a root comment via parentId (replies-to-replies aren't allowed)
await FeedbackJar.shared.addComment(postId: postId, content: "Same issue here!") name/email fall back to the remembered identity (setIdentity) when omitted.
Get widget config
Check whether the org asks for name/email, and whether voting/comments are enabled, before building your own UI around them:
if case .success(let config) = await FeedbackJar.shared.getConfig() {
if config.collectEmail { /* show an email field */ }
if config.allowVotes { /* show your vote button */ }
} | Field | Meaning |
|---|---|
collectName | Org asks submitters for their name (“Ask for Name”) |
collectEmail | Org asks submitters for their email (“Ask for Email”) |
allowVotes | Guest upvoting is enabled for this project |
allowComments | Guest commenting is enabled for this project |
API reference
| Method | Description |
|---|---|
configure(widgetId:) | Configure the SDK. Call once before anything else. |
setIdentity(name:email:) | Remember a submitter’s name/email for later calls. |
setIdentity(userId:email:signature:timestamp:...) | Set a verified, org-signed identity — see Verified identity. |
getIdentity() | The currently remembered identity, if any. |
clearIdentity() | Forget the remembered identity (e.g. on logout). |
submit(_:email:name:properties:) async | Submit feedback. Returns Result<FeedbackResponse, Error>. |
listFeedback(boardId:limit:cursor:) async | List public feedback. limit is clamped to 1–50. |
getPost(_:) async | Fetch a single public post. |
getConfig() async | Fetch this project’s WidgetConfig. |
vote(postId:) async / unvote(postId:) async | Cast or remove this install’s upvote. Idempotent. |
voteState(postId:) async | Current upvote count and whether this install voted. |
listComments(postId:limit:cursor:) async | List a post’s comments (two levels). |
addComment(postId:content:parentId:name:email:) async | Add a comment or reply. |
All async methods have a callback overload (completion: @escaping (Result<T, Error>) -> Void) safe to call from the main thread.
Notes
- Submission, voting, and comments are all anonymous by default —
setIdentityattaches a name/email, it does not require a full account or portal login. - Voting and commenting only work when the project has them enabled — check
getConfig()before showing that UI. - The server rate-limits submissions (5 per 15 minutes per IP).
- Private boards and non-public posts are never returned by
listFeedback.