admaDIC App Development & IT Solutions

SwiftUI Reusable Fields and Validators

by Annett Schwarze | 2026-02-06

In a form for editing record data reusable custom edit views EditTextField, EditAmountField and EditDateField are shown, which allows for easy construction of complex screens. Usually input validation is needed in these cases.

Reusable input validators TextFieldValidator and AmountFieldValidator are implemented to keep the actual validation logic encapsulated. Forms built in this way are easy to understand and maintain.

        
import SwiftUI

struct SampleFormEditView: View {
    struct Transaction: Identifiable {
        let id: UUID = UUID()
        var text: String
        var teamId: String
        var date: Date
        var amount: Int64
        var commission: Int64
    }

    @State private var item = Self.sampleEarning()

    @State private var newText: String = ""
    @State private var newTeamId: String = ""
    @State private var newAmountString: String = ""
    @State private var newCommissionString: String = ""
    @State private var newDate: Date = Date()

    private var currentTextError: String? { TextFieldValidator.error(for: newText) }
    private var currentTeamIdError: String? { TextFieldValidator.error(for: newTeamId) }

    private var currentAmountError: String? { AmountFieldValidator.error(for: newAmountString) }
    private var currentCommissionError: String? { AmountFieldValidator.error(for: newCommissionString) }

    static func sampleEarning() -> Transaction {
        let result = Transaction(text: "Server Configuration", teamId: "Shop-42", date: Date.now, amount: 512000, commission: 10240)
        return result
    }

    var body: some View {
        VStack(spacing: 16, content: {
            Text("Reusable Fields & Validators")
                .font(Font.largeTitle)

            VStack {
                buttonBar
                Text("Transaction")
                    .font(.title)
                Form {
                    Section("Details") {
                        EditTextField(label: "Text", value: $newText, textError: currentTextError)
                        EditTextField(label: "Team-ID", value: $newTeamId, textError: currentTeamIdError)
                        EditAmountField(label: "Amount", value: $newAmountString, amountError: currentAmountError)
                        EditAmountField(label: "Commission", value: $newCommissionString, amountError: currentCommissionError)
                        EditDateField(label: "Date", value: $newDate)
                    }
                }
            }
            .task {
                loadData()
            }
        })
    }

    private var buttonBar: some View {
        HStack {
            Button(action: { /* Close */ }, label: {
                Text("Cancel").padding().glassEffect()
            })
            .buttonStyle(BorderlessButtonStyle())
            Spacer()
            Button(action: {
                if canSaveItem() { saveItem() }
            }, label: {
                Text("Save").padding().glassEffect()
            })
            .buttonStyle(BorderlessButtonStyle())
        }
        .padding()
    }

    private func loadData() {
        newText = item.text
        newTeamId = item.teamId
        newAmountString = AmountFieldValidator.inputStringFrom(amount: item.amount)
        newCommissionString = AmountFieldValidator.inputStringFrom(amount: item.commission)
        newDate = item.date
    }

    private func canSaveItem() -> Bool {
        return
            TextFieldValidator.isValid(newText) &&
            TextFieldValidator.isValid(newTeamId) &&
            AmountFieldValidator.isValid(newAmountString) &&
            AmountFieldValidator.isValid(newCommissionString)
    }

    private func saveItem() {
        item.text = newText
        item.teamId = newTeamId
        item.amount = AmountFieldValidator.amountValueFrom(string: newAmountString)
        item.commission = AmountFieldValidator.amountValueFrom(string: newCommissionString)
        item.date = newDate
        // Save to database...
    }

    // MARK: - Reusable Fields

    struct EditTextField: View {
        let label: String
        @Binding var value: String
        let textError: String?

        var body: some View {
            VStack(alignment: .leading, spacing: 4) {
                TextField(label, text: $value).padding(.vertical, 6)
                if let textError {
                    Text(textError).font(.footnote).foregroundStyle(.red).transition(.opacity)
                }
            }
        }
    }

    struct EditAmountField: View {
        let label: String
        @Binding var value: String
        let amountError: String?

        var body: some View {
            VStack(alignment: .leading, spacing: 4) {
                TextField(label,
                    text: $value)
                .textInputAutocapitalization(.words)
                .keyboardType(.numbersAndPunctuation)
                .overlay(alignment: .trailing) { Text("€") }
                .padding(.vertical, 6)
                if let amountError {
                    Text(amountError).font(.footnote).foregroundStyle(.red).transition(.opacity)
                }
            }
        }
    }

    struct EditDateField: View {
        let label: String
        @Binding var value: Date

        var body: some View {
            VStack(alignment: .leading, spacing: 4) {
                DatePicker(label, selection: $value, displayedComponents: [.date])
                    .labelsHidden()
                .padding(.vertical, 6)
            }
        }
    }

    // MARK: - Reusable Validators

    struct TextFieldValidator {
        static func isValid(_ text: String) -> Bool {
            return !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        }

        static func error(for text: String, errorMessage: String? = nil) -> String? {
            guard !isValid(text) else { return nil }
            return errorMessage ?? "Please enter a text"
        }
    }

    struct AmountFieldValidator {
        static let inputNumberFormatter: NumberFormatter = {
            let nf = NumberFormatter()
            nf.numberStyle = .currency
            nf.positiveFormat = "#,##0.00"
            return nf
        }()

        static func isValid(_ amountString: String) -> Bool {
            let amountNumber = inputNumberFormatter.number(from: amountString)
            return amountNumber != nil
        }

        static func error(for amountString: String, errorMessage: String? = nil) -> String? {
            guard !isValid(amountString) else { return nil }
            return errorMessage ?? "Enter a valid amount"
        }

        static func amountValueFrom(string: String) -> Int64 {
            let nf = inputNumberFormatter
            let amountNumber = nf.number(from: string)
            let amountDouble = (amountNumber?.doubleValue ?? 0) * 100
            let amount = Int64(amountDouble.rounded())
            return amount
        }

        static func inputStringFrom(amount: Int64) -> String {
            let nf = inputNumberFormatter
            let amountString = nf.string(from: NSNumber(value: Double(amount)/100.0))
            return amountString ?? ""
        }
    }
}

#Preview {
    SampleFormEditView()
}
    
SwiftUI Reusable Fields and Validators

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners
Last Change: Fri Feb 6 09:50:57 2026 GMT