Features.Vote - Build profitable features from user feedback | Product Hunt
SwiftUI tutorial · iOS 16+

Add a Feature Request Board to Your iOS App

Let users submit and upvote ideas right inside your app. We'll build a native SwiftUI voting board from scratch — then look at the parts that are deceptively hard to do yourself.

See feature voting & feedback boards in action

Watch: add feedback boards & a changelog to your iOS app

"Shout out to FeaturesVote! Integration was done in under a minute"

Alexandre Negrel,

Founder at Prisme Analytics

A feature request board turns scattered "you should add…" messages into a ranked list of what users actually want. The SwiftUI for it is straightforward — a List, an upvote button, and a store. We'll build the whole thing in four steps, then be honest about what it takes to make it work across thousands of users.

1

Model a feature request

Each request needs a title, a vote count, and whether the current user has voted. A simple Identifiable struct is enough to drive a SwiftUI List.

import SwiftUI

struct FeatureRequest: Identifiable {
    let id = UUID()
    var title: String
    var votes: Int
    var hasVoted: Bool
}
2

Hold state in an ObservableObject

A store publishes the list and handles voting and adding. Toggling a vote updates the count and re-sorts so the most-wanted requests rise to the top.

@MainActor
final class FeatureStore: ObservableObject {
    @Published var requests: [FeatureRequest] = []

    func toggleVote(_ request: FeatureRequest) {
        guard let i = requests.firstIndex(where: { $0.id == request.id }) else { return }
        requests[i].hasVoted.toggle()
        requests[i].votes += requests[i].hasVoted ? 1 : -1
        requests.sort { $0.votes > $1.votes }   // most-wanted first
    }

    func add(_ title: String) {
        requests.insert(
            FeatureRequest(title: title, votes: 1, hasVoted: true),
            at: 0
        )
    }
}
3

Build the upvote button

A compact vertical vote control — a triangle and a count — that fills in when the user has voted. This is the heart of any voting board.

struct VoteButton: View {
    let votes: Int
    let hasVoted: Bool
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            VStack(spacing: 2) {
                Image(systemName: hasVoted ? "arrowtriangle.up.fill" : "arrowtriangle.up")
                Text("\(votes)").font(.caption.bold())
            }
            .frame(width: 44)
            .foregroundStyle(hasVoted ? AnyShapeStyle(.tint) : AnyShapeStyle(.secondary))
        }
        .buttonStyle(.plain)
    }
}
4

Assemble the board

Put it together in a List inside a NavigationStack, with a + button to submit new ideas via a sheet. That's a working board — for a single device.

struct FeatureBoard: View {
    @StateObject private var store = FeatureStore()
    @State private var showNew = false

    var body: some View {
        NavigationStack {
            List(store.requests) { request in
                HStack(spacing: 16) {
                    VoteButton(votes: request.votes, hasVoted: request.hasVoted) {
                        store.toggleVote(request)
                    }
                    Text(request.title)
                }
            }
            .navigationTitle("Feature Requests")
            .toolbar {
                Button { showNew = true } label: { Image(systemName: "plus") }
            }
            .sheet(isPresented: $showNew) {
                NewRequestView { store.add($0) }
            }
        }
    }
}

What the tutorial leaves out

The board above works on one device. A board that's actually useful needs three things the UI doesn't give you.

Cross-user sync

Local state only counts one person's votes. A real board needs a backend so everyone sees the same list and tallies.

Dedup & moderation

Users submit the same idea ten different ways. You need merging, and a way to hide spam before it shows.

Status & the feedback loop

Planned, in progress, shipped — plus notifying the people who voted when their request lands. That's what keeps users coming back.

The same board, hosted — in one line

If you'd rather not build and run the backend, Features.Vote gives you the same native SwiftUI board with cross-user sync, dedup, statuses and voter notifications already handled — plus a roadmap and changelog from the same data.

import SwiftUI
import FeaturesVote

struct FeatureBoard: View {
    var body: some View {
        // A hosted, cross-user board: votes sync, duplicates
        // merge, statuses update, voters get notified on ship.
        FeaturesVote.VotingBoardView()
    }
}

// One-time setup:
// FeaturesVote.configure(with: "your-project-slug")

Frequently Asked Questions

Still not convinced?

Here's a full price comparison with all top competitors

Okay, okay! Sign me up!

Start building the right features today ⚡️