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

Build a Feedback Form in SwiftUI

A clean, native feedback form is about 40 lines of SwiftUI. Here's the full thing — type picker, message editor, validation and an async submit — ready to drop into any iOS app.

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

Alexandre Negrel,

Founder at Prisme Analytics

SwiftUI's Form was made for exactly this: grouped sections, native styling, correct keyboard handling, Dynamic Type and dark mode — all for free. We'll build a complete feedback form in four steps, then look at when to reach for a ready-made component instead.

1

Model the feedback type

Start with a simple CaseIterable enum so users can categorize their feedback. Conforming to Identifiable lets you drive a Picker directly from it.

import SwiftUI enum FeedbackType: String, CaseIterable, Identifiable { case bug = "Bug" case feature = "Feature request" case other = "Other" var id: Self { self } }
2

Build the Form

SwiftUI's Form gives you native grouped sections for free. A segmented Picker for the type, a TextEditor for the message, and an optional email field is all most apps need.

struct FeedbackForm: View { @Environment(\.dismiss) private var dismiss @State private var type: FeedbackType = .bug @State private var message = "" @State private var email = "" @State private var isSubmitting = false var body: some View { NavigationStack { Form { Section("What kind of feedback?") { Picker("Type", selection: $type) { ForEach(FeedbackType.allCases) { Text($0.rawValue).tag($0) } } .pickerStyle(.segmented) } Section("Your message") { TextEditor(text: $message) .frame(minHeight: 120) } Section("Email (optional)") { TextField("you@example.com", text: $email) .keyboardType(.emailAddress) .textInputAutocapitalization(.never) } } .navigationTitle("Send Feedback") } } }
3

Validate and add a toolbar

Disable Submit until the message is meaningful, and add Cancel/Submit buttons in the navigation toolbar so the form feels like a standard iOS sheet.

// A tiny validation rule: require a meaningful message. private var isValid: Bool { message.trimmingCharacters(in: .whitespacesAndNewlines).count >= 10 } // Add this .toolbar to the Form: .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Submit") { Task { await submit() } } .disabled(!isValid || isSubmitting) } }
4

Send it somewhere

Post the feedback to your backend with async/await. Show the form as a .sheet from anywhere — a Settings row, a help screen, or a shake gesture.

func submit() async { isSubmitting = true defer { isSubmitting = false } do { try await FeedbackAPI.send(type: type, message: message, email: email) dismiss() } catch { // Surface an alert to the user here. print("Feedback failed: \(error)") } } } enum FeedbackAPI { static func send(type: FeedbackType, message: String, email: String) async throws { var req = URLRequest(url: URL(string: "https://api.yourapp.com/feedback")!) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONEncoder().encode([ "type": type.rawValue, "message": message, "email": email ]) let (_, response) = try await URLSession.shared.data(for: req) guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) } } }

Four touches that make it feel finished

Add a success state — a checkmark and 'Thanks!' beats silently dismissing the sheet.

Attach context (app version, OS, screen) automatically so you can act on bug reports.

Queue offline submissions and retry, so feedback isn't lost on a flaky connection.

Respect Dynamic Type and dark mode — Form does most of this for you if you avoid hard-coded colors.

Don't want to build the backend?

The form is easy; the storage, dashboard, dedup, status and notifications behind it are the real work. Features.Vote ships a native SwiftUI CreateFeatureView (and a voting board) with all of that handled — one line to configure.

import SwiftUI import FeaturesVote struct FeedbackScreen: View { var body: some View { // A native submit + vote view — storage, dashboard // and notifications are handled for you. FeaturesVote.CreateFeatureView() } } // 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 ⚡️