import SwiftUI // MARK: Public protocol UniversalInputSelectionElement: Identifiable, Equatable, Hashable { var icon: Image? { get } var text: String? { get } } public enum UniversalInputCollection { struct TextField { let prompt: String @Binding var text: String var focus: FocusState.Binding var fieldType: T let contentType: UITextContentType let keyboardType: UIKeyboardType let submitLabel: SubmitLabel let action: () -> Void } struct SecureField { let prompt: String @Binding var text: String var focus: FocusState.Binding var fieldType: T let submitLabel: SubmitLabel let action: () -> Void } struct DropDownMenu { let prompt: String let elements: [E] @Binding var selected: E? var focus: FocusState.Binding var fieldType: T } } // MARK: Inputs implementations extension UniversalInputCollection.TextField: View { var body: some View { TextField("", text: $text) .padding(.horizontal, 8) .focused(focus, equals: fieldType) .font(.body2) .foregroundColor(.Material.Text.main) .autocorrectionDisabled(true) .autocapitalization(.none) .textContentType(contentType) .keyboardType(keyboardType) .submitLabel(submitLabel) .textSelection(.enabled) .onSubmit { action() } .modifier(UniversalInputModifier( prompt: prompt, focus: focus, fieldType: fieldType, isActive: isFilled )) } var isFilled: Bool { !text.isEmpty || focus.wrappedValue == fieldType } } extension UniversalInputCollection.SecureField: View { var body: some View { SecureField("", text: $text) .padding(.horizontal, 8) .focused(focus, equals: fieldType) .font(.body2) .foregroundColor(.Material.Text.main) .autocorrectionDisabled(true) .autocapitalization(.none) .textContentType(.password) .submitLabel(submitLabel) .textSelection(.disabled) .onSubmit { action() } .modifier(UniversalInputModifier( prompt: prompt, focus: focus, fieldType: fieldType, isActive: isFilled )) } var isFilled: Bool { !text.isEmpty || focus.wrappedValue == fieldType } } extension UniversalInputCollection.DropDownMenu: View { var body: some View { ZStack { HStack { Text(text) .font(.body2) .foregroundColor(.Material.Text.main) .padding(.leading, 8) Spacer() } .modifier(UniversalInputModifier( prompt: prompt, focus: focus, fieldType: fieldType, isActive: selected != nil )) Menu { ForEach(elements, id: \.self.id) { element in Button { selected = element } label: { Text(element.text ?? "") } } } label: { Label("", image: "") .labelStyle(TitleOnlyLabelStyle()) .padding(.vertical) .frame(height: 48) .frame(maxWidth: .infinity) } } } var text: String { if let text = selected?.text { return text } else { return "" } } } // MARK: Modifiers private struct UniversalInputModifier: ViewModifier { let prompt: String var focus: FocusState.Binding var fieldType: T let isActive: Bool var promptBackground: Color? var isCentered: Bool? var customTapAction: (() -> Void)? func body(content: Content) -> some View { VStack(spacing: 0) { ZStack { HStack { Text(isActive ? "" : prompt) .font(.body2) .foregroundColor(.Material.Shape.separator) .padding(8) Spacer() } content .frame(height: 48) } } .frame(height: 48) .background { ZStack { RoundedRectangle(cornerRadius: 4) .foregroundColor(.Material.Shape.white) RoundedRectangle(cornerRadius: 4) .stroke(Color.Material.Shape.separator) } } .contentShape(Rectangle()) .onTapGesture { if let customTapAction { customTapAction() } else { if focus.wrappedValue != fieldType { focus.wrappedValue = fieldType } } } } } // MARK: Validators extension UniversalInputCollection { enum Validators { static func isEmail(_ input: String) -> Bool { if !input.isEmpty { let mailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" if !NSPredicate(format: "SELF MATCHES %@", mailRegex).evaluate(with: input) { return false } else { return true } } else { return true } } } }