A highly customizable and feature-rich floating label text field component for SwiftUI, inspired by Material Design principles.
- 🎨 Multiple Built-in Styles: Default, Material, Outlined, Filled, Minimal, and Custom styles
- ✅ Comprehensive Validation: Built-in validators for email, phone, URL, password, and more
- 🔄 Real-time Validation: Validate as users type with customizable error messages
- 🔒 Secure Text Entry: Built-in password field support with show/hide toggle
- 🎯 Focus Management: Smooth animations and focus state handling
- 🔧 Highly Customizable: Extensive modifier API for complete customization
- 📱 Keyboard Support: Different keyboard types and input methods
- 🖼️ Icon Support: Leading and trailing icons with SF Symbols
- 💡 Helper Text: Optional helper text and validation error messages
- 🎭 Custom Content: Support for custom leading and trailing content
- iOS 15.0+
- Xcode 16.0+
- Swift 5.5+
Add the following to your Package.swift file:
dependencies:[.package(url:"https://github.com/mohammadnabulsi/FloatingTextField.git", from:"1.0.0")]Or add it through Xcode:
- Go to
File→Add Package Dependencies - Enter the repository URL
- Click
Add Package
import SwiftUI
import FloatingTextFieldSwiftUI
structContentView:View{@Stateprivatevaremail=""@Stateprivatevarname=""varbody:someView{VStack(spacing:20){FloatingTextField($name, label:"Full Name").leadingIcon("person")FloatingTextField($email, label:"Email Address").leadingIcon("envelope").keyboardType(.emailAddress).email().required()}.padding()}}structPasswordView:View{@Stateprivatevarpassword=""@StateprivatevarisPasswordVisible=falsevarbody:someView{FloatingTextField($password, label:"Password").leadingIcon("lock").secure(!isPasswordVisible).password(minLength:8, requireNumbers:true, requireSpecialChars:true).trailingContent{Button(action:{ isPasswordVisible.toggle()}){Image(systemName: isPasswordVisible ?"eye.slash":"eye").foregroundColor(.secondary)}}.padding()}}// Default Style
FloatingTextField($text, label:"Default")
// Material Design Style
FloatingTextField($text, label:"Material").floatingTextFieldStyle(MaterialFloatingStyle())
// Outlined Style
FloatingTextField($text, label:"Outlined").floatingTextFieldStyle(OutlinedFloatingStyle())
// Filled Style
FloatingTextField($text, label:"Filled").floatingTextFieldStyle(FilledFloatingStyle())
// Minimal Style
FloatingTextField($text, label:"Minimal").floatingTextFieldStyle(MinimalFloatingStyle())
// Custom Style
FloatingTextField($text, label:"Custom").floatingTextFieldStyle(CustomFloatingStyle(
accent:.purple,
background:.purple.opacity(0.1),
border:.purple.opacity(0.3)))structValidationExample:View{@Stateprivatevaremail=""@Stateprivatevarphone=""@Stateprivatevarusername=""@Stateprivatevarage=""varbody:someView{VStack(spacing:16){
// Email validation
FloatingTextField($email, label:"Email").leadingIcon("envelope").email("Please enter a valid email address").required("Email is required").validateRealTime()
// Phone number validation
FloatingTextField($phone, label:"Phone Number").leadingIcon("phone").phoneNumber("Please enter a valid phone number").keyboardType(.phonePad)
// Username validation
FloatingTextField($username, label:"Username").leadingIcon("at").alphanumeric("Only letters and numbers allowed").minLength(3, errorMessage:"Username must be at least 3 characters").maxLength(20, errorMessage:"Username cannot exceed 20 characters")
// Age validation
FloatingTextField($age, label:"Age").leadingIcon("calendar").numeric("Please enter a valid number").keyboardType(.numberPad)}.padding()}}FloatingTextField($text, label:"Custom Validation").validation([ValidationRule(
condition:{ !$0.isEmpty },
errorMessage:"This field cannot be empty"),ValidationRule(
condition:{ $0.count >=5},
errorMessage:"Must be at least 5 characters"),ValidationRule(
condition:{ $0.contains("@")},
errorMessage:"Must contain @ symbol")]).validateRealTime(true).onValidationChange{ isValid inprint("Field is valid: \(isValid)")}structRegistrationForm:View{@StateprivatevarfullName=""@Stateprivatevaremail=""@Stateprivatevarpassword=""@StateprivatevarconfirmPassword=""@Stateprivatevarwebsite=""@StateprivatevarisPasswordVisible=false@StateprivatevarisFormValid=falsevarbody:someView{ScrollView{VStack(spacing:20){Text("Create Account").font(.title2).fontWeight(.bold)FloatingTextField($fullName, label:"Full Name").leadingIcon("person").required("Please enter your full name").alphabetic("Name should only contain letters").minLength(2).floatingTextFieldStyle(MaterialFloatingStyle()).onValidationChange{ isValid inupdateFormValidation()}FloatingTextField($email, label:"Email Address").leadingIcon("envelope").keyboardType(.emailAddress).required("Email address is required").email("Please enter a valid email format").floatingTextFieldStyle(OutlinedFloatingStyle()).onValidationChange{ isValid inupdateFormValidation()}FloatingTextField($password, label:"Password").leadingIcon("lock").secure(!isPasswordVisible).password(
minLength:8,
requireNumbers:true,
requireSpecialChars:true,
errorMessage:"Password must be 8+ characters with numbers and special characters").trailingContent{Button(action:{ isPasswordVisible.toggle()}){Image(systemName: isPasswordVisible ?"eye.slash":"eye").foregroundColor(.secondary)}}.floatingTextFieldStyle(FilledFloatingStyle()).onValidationChange{ isValid inupdateFormValidation()}FloatingTextField($confirmPassword, label:"Confirm Password").leadingIcon("lock").secure(true).validation([ValidationRule(
condition:{ !$0.isEmpty },
errorMessage:"Please confirm your password"),ValidationRule(
condition:{ $0 == password },
errorMessage:"Passwords don't match")]).onValidationChange{ isValid inupdateFormValidation()}FloatingTextField($website, label:"Website").leadingIcon("globe").keyboardType(.URL).url("Please enter a valid URL").helperText("Optional: Enter your website URL").floatingTextFieldStyle(MinimalFloatingStyle())Button(action: submitForm){Text("Create Account").fontWeight(.semibold).frame(maxWidth:.infinity).padding().background(isFormValid ?Color.blue :Color.gray).foregroundColor(.white).cornerRadius(12)}.disabled(!isFormValid)}.padding()}}privatefunc updateFormValidation(){
// Custom form validation logic
isFormValid = !fullName.isEmpty && !email.isEmpty && !password.isEmpty && !confirmPassword.isEmpty
}privatefunc submitForm(){print("Form submitted successfully!")}}| Modifier | Description | Example |
|---|---|---|
.floatingTextFieldStyle(_:) | Apply a custom style | .floatingTextFieldStyle(MaterialFloatingStyle()) |
.secure(_:) | Enable/disable secure text entry | .secure(true) |
.keyboardType(_:) | Set keyboard type | .keyboardType(.emailAddress) |
.leadingIcon(_:) | Add leading icon | .leadingIcon("envelope") |
.trailingContent(_:) | Add custom trailing content | .trailingContent { Button(...) } |
.leadingContent(_:) | Add custom leading content | .leadingContent { Image(...) } |
.helperText(_:) | Set helper text | .helperText("Optional field") |
.enabled(_:) | Enable/disable field | .enabled(false) |
.validateRealTime(_:) | Enable real-time validation | .validateRealTime(true) |
.onValidationChange(_:) | Validation state callback | .onValidationChange { isValid in ... } |
| Modifier | Description | Example |
|---|---|---|
.required(_:) | Make field required | .required("This field is required") |
.email(_:) | Email validation | .email("Invalid email format") |
.phoneNumber(_:) | Phone number validation | .phoneNumber("Invalid phone number") |
.url(_:) | URL validation | .url("Invalid URL") |
.minLength(_:errorMessage:) | Minimum length | .minLength(8, errorMessage: "Too short") |
.maxLength(_:errorMessage:) | Maximum length | .maxLength(50, errorMessage: "Too long") |
.alphanumeric(_:) | Alphanumeric only | .alphanumeric("Letters and numbers only") |
.alphabetic(_:) | Alphabetic only | .alphabetic("Letters only") |
.numeric(_:) | Numeric only | .numeric("Numbers only") |
.password(...) | Password validation | .password(minLength: 8, requireNumbers: true) |
.validation(_:) | Custom validation rules | .validation([ValidationRule(...)]) |
This project is licensed under the MIT License - see the LICENSE file for details.
- Inspired by Material Design floating label text fields
- Built with ❤️ for the SwiftUI community
If you have any questions or need help, please open an issue on GitHub.
⭐ Star this repo if you found it helpful! ⭐
