Part 4: Rethinking Feedback - Building a Native Feedback Form with FormLogger
Let's bring together everything we've covered so far - from setting up the backend to securing the data flow - and build a seamless, native feedback form that integrates directly into your SwiftUI app.
With FormLogger
, we'll tie it all together in a way that feels intuitive for developers and effortless for users.
Forget email clients, public GitHub issues, or sliding into social DMs. This is about a clean, native experience that just works.
What Is FormLogger?
If you're just joining us, FormLogger
is a Swift package designed to make collecting feedback, bug reports, and feature requests as smooth as possible within your app.
Its purpose is simple: automate the entire submission pipeline - from input validation to backend delivery - while keeping you in full control of how and where that data is handled.
Whether users are reporting a bug, suggesting a feature, or just leaving feedback, FormLogger
takes care of the heavy lifting.
Here's what it does for you:
- Validates Input - Ensures required fields like title, description, and contact info are correctly filled out.
- Handles Networking - Manages the secure submission of feedback to your backend.
- Integrates Seamlessly - Works with Cloudflare Workers (or your backend of choice) to store feedback or generate GitHub issues automatically.
With FormLogger, you get a robust, easy-to-implement feedback system-without any of the complexity.
What We're Building
We're creating a native feedback form within your app, allowing users to send feedback, bug reports, or feature requests directly to your backend.
Here's the vision:
- Users submit feedback via a SwiftUI form.
- That form sends the data to a Cloudflare Worker, which processes it and submits it as a GitHub issue (or integrates with your preferred system).
- Optionally, the form can bundle log data to help with debugging.
- Real-time validation and submission feedback keep the user informed throughout the process.
Usage
Step 1: Set Up Your Project
We'll start by creating a new SwiftUI app. Here's the entry point for a simple test project:
@main
struct TestFormApp: App {
var body: some Scene {
WindowGroup {
ContactFormView(
viewModel: FormManager(
formType: .bug,
configuration: PreviewRepository.singleRepoConfig
)
)
}
}
}
This sets the root view to ContactFormView
, which is initialised with a FormManager
. The manager handles the form's state, including the type of feedback and any configuration options.
Step 2: Define the Form Configuration
Next, let's define how the form behaves-what it connects to, how it limits input, and what kind of logs it attaches.
struct PreviewFormConfig: FormConfiguration {
var apiURL: URL = URL(string: "http://localhost:8787")!
var characterLimit: Int = 200
var shouldClearForm: Bool = true
var clearFormDelay: TimeInterval = 1
var loggerManager: LoggerManager = .last24HoursWithSystem
var repository: RepositoryResolver
}
Here's what each setting does:
apiURL
points to your backend-this can be local for testing or your production endpoint.- Character limits help manage input size and readability.
- Logging is configured to automatically include the last 24 hours of logs.
repository
determines where the feedback goes based on its type.
Step 3: Build the Contact Form UI
Here's the SwiftUI view that powers the form, capturing input and managing submission:
struct ContactFormView: View {
@State private var viewModel: FormManager
init(viewModel: FormManager) {
_viewModel = State(initialValue: viewModel)
}
var body: some View {
Form {
// Feedback Type
Section(header: Text("Feedback Type")) {
Text(viewModel.formType.description)
}
// Title Section
Section(header: Text("Title")) {
TextField("Enter a title", text: Binding(
get: { viewModel.userInput.title },
set: { viewModel.userInput = .init(
title: $0,
description: viewModel.userInput.description,
contact: viewModel.userInput.contact
)}
))
}
// Description Section
Section(header: Text("Description")) {
TextEditor(text: Binding(
get: { viewModel.userInput.description },
set: { viewModel.userInput = .init(
title: viewModel.userInput.title,
description: $0,
contact: viewModel.userInput.contact
)}
))
.frame(minHeight: 100)
}
// Contact Info Section
Section {
Toggle("Include Contact Info", isOn: $viewModel.allowContact)
if viewModel.allowContact {
TextField("Your Name", text: Binding(
get: { viewModel.userInput.contact?.name ?? "" },
set: { name in
let email = viewModel.userInput.contact?.email ?? ""
viewModel.userInput = .init(
title: viewModel.userInput.title,
description: viewModel.userInput.description,
contact: .init(name: name, email: email)
)
}
))
TextField("Your Email", text: Binding(
get: { viewModel.userInput.contact?.email ?? "" },
set: { email in
let name = viewModel.userInput.contact?.name ?? ""
viewModel.userInput = .init(
title: viewModel.userInput.title,
description: viewModel.userInput.description,
contact: .init(name: name, email: email)
)
}
))
.keyboardType(.emailAddress)
}
}
// Buttons for Submission and Autofill
Section {
Button("Submit") {
Task {
do {
let result = try await viewModel.submit()
print("Form result: \(result)")
} catch {
print("Error: \(error)")
}
}
}
.disabled(!viewModel.isFormValid || viewModel.isProcessing)
}
}
}
}
The form includes:
- Title and description fields as the main input.
- Optional contact info, toggled by the user.
- A submit button that validates and sends the form.
Step 4: Testing and Deployment
You can test the form in the iOS Simulator or on a physical device. If you're working locally, keep your apiURL
pointing to http://localhost:8787
.
When you're ready to ship, simply update the URL to your live Cloudflare Worker endpoint:
var apiURL: URL = URL(string: "https://feedback-worker.username.workers.dev")!
Conclusion
With this setup, you now have a fully integrated, native feedback solution built right into your SwiftUI app. FormLogger
handles the validation, submission, and logging-freeing you up to focus on what matters: improving your product.
You've successfully:
- Built a native SwiftUI feedback form.
- Integrated it using the
FormLogger
package. - Connected the form to a backend via Cloudflare Workers.
- Enabled optional logging and real-time validation.
This approach is both scalable and maintainable - ideal for collecting anything from bug reports to general feedback. You're now well equipped to turn user input into meaningful insights.
If you found this article helpful, you can keep the ideas flowing by supporting me. Buy me a coffee or check out my apps to help me create more content like this!
Coffee Check out my apps