204 lines
5.7 KiB
Swift
204 lines
5.7 KiB
Swift
|
import SwiftUI
|
||
|
|
||
|
// MARK: Public
|
||
|
protocol UniversalInputSelectionElement: Identifiable, Equatable, Hashable {
|
||
|
var icon: Image? { get }
|
||
|
var text: String? { get }
|
||
|
}
|
||
|
|
||
|
public enum UniversalInputCollection {
|
||
|
struct TextField<T: Hashable> {
|
||
|
let prompt: String
|
||
|
@Binding var text: String
|
||
|
var focus: FocusState<T?>.Binding
|
||
|
var fieldType: T
|
||
|
let contentType: UITextContentType
|
||
|
let keyboardType: UIKeyboardType
|
||
|
let submitLabel: SubmitLabel
|
||
|
let action: () -> Void
|
||
|
}
|
||
|
|
||
|
struct SecureField<T: Hashable> {
|
||
|
let prompt: String
|
||
|
@Binding var text: String
|
||
|
var focus: FocusState<T?>.Binding
|
||
|
var fieldType: T
|
||
|
let submitLabel: SubmitLabel
|
||
|
let action: () -> Void
|
||
|
}
|
||
|
|
||
|
struct DropDownMenu<T: Hashable, E: UniversalInputSelectionElement> {
|
||
|
let prompt: String
|
||
|
let elements: [E]
|
||
|
@Binding var selected: E?
|
||
|
var focus: FocusState<T?>.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<T: Hashable>: ViewModifier {
|
||
|
let prompt: String
|
||
|
var focus: FocusState<T?>.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
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|