Skip to content

Form Components

PowerSyncForm provides React Hook Form-like APIs for building type-safe, validated forms in SwiftUI.

import SwiftUI
import ZyraForm
struct UserFormView: View {
@StateObject var form = PowerSyncForm<UserFormValues>(schema: UsersSchema)
var body: some View {
Form {
// Form fields here
}
}
}

Define your form values struct:

struct UserFormValues: FormValues {
var email: String = ""
var name: String = ""
var age: Int?
var website: String?
init() {}
}

Create bindings for TextField components:

// String binding (validates on change)
if let emailBinding = form.binding(for: "email") {
TextField("Email", text: emailBinding)
}
// String binding with onBlur validation
if let emailBinding = form.bindingWithBlur(for: "email") {
TextField("Email", text: emailBinding)
.onBlur {
form.handleBlur("email")
}
}
// Integer binding
if let ageBinding = form.intBinding(for: "age") {
TextField("Age", text: ageBinding)
.keyboardType(.numberPad)
}
// Double binding
if let priceBinding = form.doubleBinding(for: "price") {
TextField("Price", text: priceBinding)
.keyboardType(.decimalPad)
}
// Boolean binding
if let isActiveBinding = form.boolBinding(for: "is_active") {
Toggle("Is Active", isOn: isActiveBinding)
}

Show validation errors:

TextField("Email", text: form.binding(for: "email") ?? .constant(""))
if form.hasError("email") {
Text(form.getError("email") ?? "")
.foregroundColor(.red)
.font(.caption)
}

Access form state properties:

form.isValid // Bool: Is form valid?
form.isDirty // Bool: Has form been modified?
form.isSubmitting // Bool: Is form submitting?
form.errors // FormErrors: All form errors

Choose when validation occurs:

// Validate on every change (default)
@StateObject var form = PowerSyncForm<UserFormValues>(
schema: UsersSchema,
mode: .onChange
)
// Validate on blur
@StateObject var form = PowerSyncForm<UserFormValues>(
schema: UsersSchema,
mode: .onBlur
)
// Validate on submit only
@StateObject var form = PowerSyncForm<UserFormValues>(
schema: UsersSchema,
mode: .onSubmit
)
// Validate once field is touched
@StateObject var form = PowerSyncForm<UserFormValues>(
schema: UsersSchema,
mode: .onTouched
)

Handle form submission with validation:

// Synchronous handler
Button("Submit") {
form.handleSubmit { values in
print("Form values:", values)
// Save to database
}
}
// Async handler
Button("Submit") {
form.handleSubmit { values async throws in
try await saveToDatabase(values)
}
}
// Result-based submission
Button("Submit") {
let result = form.submit()
switch result {
case .success(let values):
print("Success:", values)
case .failure(let errors):
print("Errors:", errors.allErrors)
}
}
// Reset to initial values
form.reset()
// Reset to new values
form.reset(to: UserFormValues(email: "new@example.com"))
// Manually validate
let isValid = form.validate()
// Validate single field
form.validateField("email")
// Set field value
form.setValue("test@example.com", for: "email")
// Set multiple values
form.setValues([
"email": "user@example.com",
"name": "John Doe"
])
// Get field value
let email = form.getValue(for: "email")
// Get all values
let allValues = form.getValues()
// Update values from dictionary (useful for server responses)
form.updateValues([
"email": "updated@example.com",
"name": "Updated Name"
])
// Change validation mode dynamically
form.setMode(.onBlur)

Watch field values for conditional logic:

// Watch single field
let email = form.watch("email")
// Watch multiple fields
let fields = form.watch(["email", "name"])
// Watch all fields
let allFields = form.watchAll()
struct RegistrationFormView: View {
@StateObject var form = PowerSyncForm<RegistrationFormValues>(
schema: UsersSchema,
mode: .onChange
)
var body: some View {
Form {
Section("Account Information") {
if let emailBinding = form.binding(for: "email") {
TextField("Email", text: emailBinding)
.textContentType(.emailAddress)
.autocapitalization(.none)
if form.hasError("email") {
Text(form.getError("email") ?? "")
.foregroundColor(.red)
.font(.caption)
}
}
if let nameBinding = form.binding(for: "name") {
TextField("Name", text: nameBinding)
if form.hasError("name") {
Text(form.getError("name") ?? "")
.foregroundColor(.red)
.font(.caption)
}
}
}
Section {
Button(action: {
form.handleSubmit { values in
Task {
await saveUser(values)
}
}
}) {
Text("Register")
.frame(maxWidth: .infinity)
}
.disabled(!form.isValid || form.isSubmitting)
Button("Reset") {
form.reset()
}
}
}
.navigationTitle("Registration")
}
private func saveUser(_ values: RegistrationFormValues) async {
// Save logic here
}
}

Create a reusable form field component:

struct FormField<Content: View>: View {
let field: String
@ObservedObject var form: PowerSyncForm<GenericFormValues>
let content: (Binding<String>) -> Content
var body: some View {
VStack(alignment: .leading, spacing: 4) {
if let binding = form.binding(for: field) {
content(binding)
.textFieldStyle(.roundedBorder)
if form.hasError(field) {
Text(form.getError(field) ?? "")
.foregroundColor(.red)
.font(.caption)
}
}
}
}
}
// Usage
FormField(field: "email", form: form) { binding in
TextField("Email", text: binding)
}