Skip to content
VoteFirst Docs
Dashboard

Swift

Swift quickstart

A working feedback board inside your iOS or macOS app, in three steps. One key to set it up, one line to show it.

The shape of it: add the package, hand it your embed key, and place a view. The board brings its own navigation, so it works on a tab, in a sheet, or on a screen of its own, with nothing to wrap it in.

  1. Add the package

    In Xcode, File → Add Package Dependencies, paste the repository address, and add the VoteFirst library to your app target.

    The repository
    https://github.com/votefirst-app/votefirst-swift

    In a package of your own, it is a dependency and a product.

    Package.swift
    dependencies: [
        .package(url: "https://github.com/votefirst-app/votefirst-swift", from: "0.3.0")
    ],
    targets: [
        .target(name: "MyApp", dependencies: [
            .product(name: "VoteFirst", package: "votefirst-swift")
        ])
    ]

    While the views settle, .upToNextMinor(from: "0.3.0") is the safer requirement. The package is 0.x, so a minor release can change them.

  2. Start it with your embed key

    Open the dashboard, choose your project, and go to Share & Embed. The embed key is the first thing on the page. It is the same key a web page uses, and it is the only value the package needs, because the key names its own board.

    MyApp.swift
    import SwiftUI
    import VoteFirst
    
    @main
    struct MyApp: App {
        init() { VoteFirst.start("vf_pk_...") }
    
        var body: some Scene { WindowGroup { RootView() } }
    }

    In UIKit, application(_:didFinishLaunchingWithOptions:) is the same moment.

    start is cheap and makes no request. It builds the client and returns. The first request happens when a view appears or when you make a call.

  3. Show a board

    One view. On a tab, it is a tab.

    In your app
    TabView {
        Home()
        VoteFirst.FeedbackView()
            .tabItem { Label("Feedback", systemImage: "lightbulb") }
    }

    Inside a settings list, use the link, which pushes the board onto your own navigation and keeps your back button.

    In your app
    List {
        Section("Support") {
            VoteFirst.FeedbackLink()
        }
    }

    UIKit gets a view controller, with its title and its tab bar item already set.

    In your app
    present(VoteFirst.viewController(), animated: true)

    That is the whole integration. Run the app and your features are there, ranked, with a vote button on every row, a filter across your stages, threaded comments, and a suggestion form behind the plus.

The whole app

The three steps together, as one file.

MyApp.swift
import SwiftUI
import VoteFirst

@main
struct MyApp: App {
    init() { VoteFirst.start("vf_pk_...") }

    var body: some Scene {
        WindowGroup {
            TabView {
                Home()
                    .tabItem { Label("Home", systemImage: "house") }
                VoteFirst.FeedbackView()
                    .tabItem { Label("Feedback", systemImage: "lightbulb") }
            }
        }
    }
}

The three products

One import gets you everything, because VoteFirst re-exports the other two.

ProductWhat it holdsWhen to use it
VoteFirstBoth of the othersAn app. This is the one to depend on
VoteFirstKitThe client, the models, the queueNo SwiftUI in it. A server side Swift app, a command line tool, a Linux build
VoteFirstUIThe screensIt already depends on the kit, so there is rarely a reason to name it directly

What it needs

PlatformsiOS 15, macOS 12, and later
Swift tools5.9
Language modesSwift 5 and Swift 6, both under strict concurrency
Xcode15 or later
DependenciesNone

Measured on macOS arm64, release, stripped, the client alone adds 451 KB to a binary, and the client with the screens adds 1078 KB.

Your embed key

It starts with vf_pk_ and it is public by design, the way any key that has to reach a browser is. It opens exactly what your public board already shows to anyone: reading the board, reading features, comments and releases, and taking part as one voter. It cannot read another project, cannot see a private one, and cannot reach anything in your dashboard.

An embed key is not an API key. The keys under API keys on the same dashboard page are secrets that open every administrative route, including exporting your voters. One of those in a shipped binary is a compromise, and every copy of your app hands it out. The package refuses to build a client with one rather than warning about it here.

No client, and an assertion in debug
VoteFirst.start("sk_live_...")

Rotating the key

Rotating stops every copy of your app that carries the old one. A web page is redeployed in a minute; a shipped app waits for review and then for people to update. Treat rotation as a release rather than a setting, and expect to ship an update behind it.

The origin allowlist does not apply

The Allowed origins box beside the key is a browser control. An app sends no Origin header, so nothing in that list can refuse it. Restricting origins for your website does not lock your app out, and it does not make the key a secret either.

What the key may spend

Per project, 3000 reads, 600 writes and 300 new voters a minute. Every copy of your app draws from the same budget rather than getting one each. The numbers, and the burst inside each of them.

Naming the project as well

In your app
VoteFirst.start(project: "your-project-slug", key: "vf_pk_...")

The same thing with the slug already known. Every route but one hangs off the slug, and the key alone has to ask for it once per launch before the first of them. Naming it saves that request.

One difference, and it only matters if you switch between the two forms. What the device stores is filed under the slug when you supply one, and under a digest of the key when you do not, so switching form looks like a different device: a new anonymous voter and an empty queue. Pick one and stay on it.

Another host

In your app
VoteFirst.start("vf_pk_...", host: URL(string: "http://localhost:18080")!)

The default is https://app.votefirst.app. The host is there for a local stack or a staging deployment. It is not a place to put a path: the package appends api/v3/... to whatever you give it.

When the key is wrong

Nothing throws at start, because a screen showing what is wrong is more use than a crash on launch.

In your app
if let problem = VoteFirst.setupError {
    print(problem.errorDescription ?? "")
}
  • VoteFirst.client is nil.
  • VoteFirst.setupError holds the reason.
  • Every built in view draws a notice with that reason on it instead of a board.
  • Every call throws it.
  • Debug builds trip an assertion, so this does not get past the first run.

A key with the right shape but no board behind it cannot be caught here, because that is not knowable without asking. It arrives on the first request as invalidKey.

What happens on the first screen

A built in screen, appearing for the first time, makes these requests in this order.

  1. GET /api/v3/board, only when the key named no project, to learn the slug.
  2. GET /api/v3/projects/{slug} for the board: columns, tags, theme, and what is allowed.
  3. POST /api/v3/projects/{slug}/identity to mint or renew this device's voter.
  4. GET /api/v3/projects/{slug}/features for the first page.

The board is held for the life of the process and shared by every screen, so a second tab costs one features call rather than four requests. The identity call is what gives the vote buttons the right state before anybody presses one.

If you never show a built in screen, nothing is minted until your first write or your first me().

Without SwiftUI

The façade holds one client and one board, which is what an app wants. A tool with no SwiftUI in it, or an app showing two different projects, can hold its own.

VoteFirstKit alone
let client = try VoteFirstClient(key: "vf_pk_...")
let board = try await client.board()
let page = try await client.features(FeatureQuery(sort: .top))

It throws for a key without the vf_pk_ prefix, and that is the only reason it throws. Two clients over two projects keep separate voters, separate queues and separate files, because everything stored is filed under the project.

Every member is in the reference.

Next