SwiftUI Reusable Fields and Validators
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()
}