iOS Keyboard Input Patterns
iOS Keyboard & Input Patterns

Comprehensive guide to keyboard handling, text input, and hardware keyboard support.
# iOS Keyboard & Input Patterns Comprehensive guide to keyboard handling, text input, and hardware keyboard support. ## Table of Contents 1. [Software Keyboard Types](#software-keyboard-types) 2. [Text Content Types](#text-content-types) 3. [Input Accessory Views](#input-accessory-views) 4. [Keyboard Avoidance](#keyboard-avoidance) 5. [Hardware Keyboard Support](#hardware-keyboard-support) 6. [Text Editing](#text-editing) 7. [Secure Text Entry](#secure-text-entry) 8. [Search Input](#search-input) 9. [Common Patterns](#common-patterns) ---
Overview
Comprehensive guide to keyboard handling, text input, and hardware keyboard support.
Manual install (npx, ZIP)
Claude Code
npx skills add wondelai/skills --skill ios-keyboard-input-patterns -g -a claude-codeClaude.ai / desktop
Codex, Cursor, other agents
Connects with Agent Alley MCP · agentalley.io/mcp
Files included
keyboard-input.md11.5 KB
Read keyboard-input.md here
wondelai/
keyboard-input.md
Show the full text467 lines
iOS Keyboard & Input Patterns
Comprehensive guide to keyboard handling, text input, and hardware keyboard support.
Table of Contents
- Software Keyboard Types
- Text Content Types
- Input Accessory Views
- Keyboard Avoidance
- Hardware Keyboard Support
- Text Editing
- Secure Text Entry
- Search Input
- Common Patterns
Software Keyboard Types
Choosing the Right Keyboard
Match the keyboard to the expected input:
| Input Type | Keyboard | UIKeyboardType |
|---|---|---|
| General text | Default | .default |
| Email-optimized (@ and . prominent) | .emailAddress |
|
| URL | URL-optimized (/, .com) | .URL |
| Phone number | Number pad | .phonePad |
| Numeric (with punctuation) | Numbers + punctuation | .numbersAndPunctuation |
| Numeric only | Decimal pad | .decimalPad |
| Twitter handle | Twitter keyboard | .twitter |
| Web search | Search keyboard | .webSearch |
| ASCII only | ASCII-capable | .asciiCapable |
SwiftUI Implementation
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.autocapitalization(.none)
.disableAutocorrection(true)
UIKit Implementation
textField.keyboardType = .emailAddress
textField.textContentType = .emailAddress
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
Text Content Types
Enable autofill by specifying content types:
| Content | textContentType | Enables |
|---|---|---|
| Name | .name |
Contact autofill |
| Given name | .givenName |
|
| Family name | .familyName |
|
.emailAddress |
Email autofill | |
| Phone | .telephoneNumber |
Phone autofill |
| Address | .streetAddressLine1 |
Address autofill |
| City | .addressCity |
|
| State | .addressState |
|
| ZIP | .postalCode |
|
| Country | .countryName |
|
| Credit card | .creditCardNumber |
Camera card scanning |
| Username | .username |
Keychain autofill |
| Password | .password |
Keychain autofill |
| New password | .newPassword |
Password generation |
| One-time code | .oneTimeCode |
SMS autofill |
Password and Login Fields
// Login form
TextField("Email", text: $email)
.textContentType(.username)
.keyboardType(.emailAddress)
SecureField("Password", text: $password)
.textContentType(.password)
// Registration form
SecureField("Create Password", text: $newPassword)
.textContentType(.newPassword)
One-Time Code (2FA)
TextField("Verification Code", text: $code)
.textContentType(.oneTimeCode)
.keyboardType(.numberPad)
iOS automatically suggests codes from SMS messages when this content type is set.
Input Accessory Views
When to Use
Input accessory views appear above the keyboard for:
- Navigation between fields (Previous/Next)
- Custom actions (Done, formatting buttons)
- Context-specific tools
Standard Toolbar Pattern
struct FormTextField: View {
@Binding var text: String
@FocusState private var isFocused: Bool
var body: some View {
TextField("Value", text: $text)
.focused($isFocused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
isFocused = false
}
}
}
}
}
Multi-Field Navigation
struct FormView: View {
@FocusState private var focusedField: Field?
enum Field {
case firstName, lastName, email
}
var body: some View {
Form {
TextField("First Name", text: $firstName)
.focused($focusedField, equals: .firstName)
TextField("Last Name", text: $lastName)
.focused($focusedField, equals: .lastName)
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button(action: focusPrevious) {
Image(systemName: "chevron.up")
}
.disabled(!canFocusPrevious)
Button(action: focusNext) {
Image(systemName: "chevron.down")
}
.disabled(!canFocusNext)
Spacer()
Button("Done") {
focusedField = nil
}
}
}
}
}
Keyboard Avoidance
Automatic Behavior (SwiftUI)
SwiftUI automatically adjusts for keyboard in most cases:
- ScrollViews scroll to keep focused field visible
- Safe area adjusts for keyboard height
Manual Keyboard Handling
struct KeyboardAdaptive: ViewModifier {
@State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
content
.padding(.bottom, keyboardHeight)
.onReceive(Publishers.keyboardHeight) { height in
withAnimation(.easeOut(duration: 0.25)) {
keyboardHeight = height
}
}
}
}
Best Practices
- Scroll to show field - Focused field should be visible
- Don't cover important actions - Submit buttons should remain accessible
- Animate adjustments - Match keyboard animation (0.25s ease-out)
- Test on different devices - Keyboard heights vary
Hardware Keyboard Support
Why It Matters
iPads commonly use hardware keyboards. Apps should:
- Support standard keyboard shortcuts
- Provide discoverability
- Not break when hardware keyboard is attached
Standard Shortcuts to Support
| Shortcut | Action | Notes |
|---|---|---|
| ⌘N | New item | Standard creation |
| ⌘S | Save | |
| ⌘⌫ | Delete | With confirmation |
| ⌘F | Find/Search | |
| ⌘Z | Undo | |
| ⌘⇧Z | Redo | |
| ⌘C/⌘V/⌘X | Copy/Paste/Cut | |
| ⌘, | Settings/Preferences | |
| ⌘W | Close window/modal | |
| Escape | Cancel/dismiss | |
| Tab | Next field | |
| ⇧Tab | Previous field | |
| Return | Submit form | When appropriate |
SwiftUI Keyboard Shortcuts
struct ContentView: View {
var body: some View {
NavigationStack {
ItemList()
.toolbar {
Button("New Item", action: createItem)
.keyboardShortcut("n", modifiers: .command)
}
}
}
}
// For custom shortcuts in list items
List {
ForEach(items) { item in
ItemRow(item: item)
}
}
.onDeleteCommand(perform: deleteSelected) // ⌘⌫
Keyboard Shortcut Discoverability
Hold ⌘ to show available shortcuts. Ensure your shortcuts appear:
Button("Save", action: save)
.keyboardShortcut("s", modifiers: .command)
// Appears in keyboard shortcut overlay
Text Editing
Autocapitalization
| Style | Use For |
|---|---|
.sentences |
General text, messages |
.words |
Names, titles |
.allCharacters |
Codes, abbreviations |
.none |
Email, usernames, URLs |
Autocorrection
| Setting | Use For |
|---|---|
| Enabled (default) | Prose, messages |
| Disabled | Code, usernames, specific values |
TextField("Username", text: $username)
.autocapitalization(.none)
.disableAutocorrection(true)
Text Input Traits Summary
TextField("Email", text: $email)
.keyboardType(.emailAddress) // Keyboard layout
.textContentType(.emailAddress) // Autofill hint
.autocapitalization(.none) // No auto-caps
.disableAutocorrection(true) // No autocorrect
.textInputAutocapitalization(.never) // iOS 15+
Secure Text Entry
Password Fields
SecureField("Password", text: $password)
.textContentType(.password)
Show/Hide Toggle Pattern
struct PasswordField: View {
@Binding var password: String
@State private var isSecure = true
var body: some View {
HStack {
if isSecure {
SecureField("Password", text: $password)
} else {
TextField("Password", text: $password)
}
Button(action: { isSecure.toggle() }) {
Image(systemName: isSecure ? "eye.slash" : "eye")
.foregroundColor(.secondary)
}
}
.textContentType(.password)
}
}
Search Input
Search Field Behavior
struct SearchView: View {
@State private var searchText = ""
var body: some View {
NavigationStack {
List(filteredItems) { item in
ItemRow(item: item)
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search items"
)
}
}
}
Search Suggestions
.searchable(text: $searchText) {
ForEach(suggestions) { suggestion in
Text(suggestion.name)
.searchCompletion(suggestion.name)
}
}
Common Patterns
Form with All Best Practices
struct RegistrationForm: View {
@State private var email = ""
@State private var password = ""
@State private var confirmPassword = ""
@FocusState private var focusedField: Field?
enum Field: Hashable {
case email, password, confirmPassword
}
var body: some View {
Form {
Section("Account") {
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textContentType(.username)
.autocapitalization(.none)
.disableAutocorrection(true)
.focused($focusedField, equals: .email)
.submitLabel(.next)
.onSubmit { focusedField = .password }
SecureField("Password", text: $password)
.textContentType(.newPassword)
.focused($focusedField, equals: .password)
.submitLabel(.next)
.onSubmit { focusedField = .confirmPassword }
SecureField("Confirm Password", text: $confirmPassword)
.textContentType(.newPassword)
.focused($focusedField, equals: .confirmPassword)
.submitLabel(.done)
.onSubmit(register)
}
Section {
Button("Create Account", action: register)
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Previous") { moveFocus(-.previous) }
Button("Next") { moveFocus(.next) }
Spacer()
Button("Done") { focusedField = nil }
}
}
}
}
Submit Label Options
| Label | Use For |
|---|---|
.done |
Final field, closes keyboard |
.go |
Triggers action (search, navigate) |
.next |
Moves to next field |
.return |
Inserts newline (text areas) |
.search |
Search field |
.send |
Message composition |
.continue |
Multi-step forms |
.join |
Joining/connecting |
.route |
Navigation apps |
TextField("Search", text: $query)
.submitLabel(.search)
.onSubmit { performSearch() }
| 1 | # iOS Keyboard & Input Patterns |
| 2 | |
| 3 | Comprehensive guide to keyboard handling, text input, and hardware keyboard support. |
| 4 | |
| 5 | |
| 6 | ## Table of Contents |
| 7 | [Software Keyboard Types] |
| 8 | [Text Content Types] |
| 9 | [Input Accessory Views] |
| 10 | [Keyboard Avoidance] |
| 11 | [Hardware Keyboard Support] |
| 12 | [Text Editing] |
| 13 | [Secure Text Entry] |
| 14 | [Search Input] |
| 15 | [Common Patterns] |
| 16 | |
| 17 | |
| 18 | |
| 19 | ## Software Keyboard Types |
| 20 | |
| 21 | ### Choosing the Right Keyboard |
| 22 | |
| 23 | Match the keyboard to the expected input: |
| 24 | |
| 25 | | Input Type | Keyboard | UIKeyboardType | |
| 26 | |------------|----------|----------------| |
| 27 | | General text | Default | `.default` | |
| 28 | | Email | Email-optimized (@ and . prominent) | `.emailAddress` | |
| 29 | | URL | URL-optimized (/, .com) | `.URL` | |
| 30 | | Phone number | Number pad | `.phonePad` | |
| 31 | | Numeric (with punctuation) | Numbers + punctuation | `.numbersAndPunctuation` | |
| 32 | | Numeric only | Decimal pad | `.decimalPad` | |
| 33 | | Twitter handle | Twitter keyboard | `.twitter` | |
| 34 | | Web search | Search keyboard | `.webSearch` | |
| 35 | | ASCII only | ASCII-capable | `.asciiCapable` | |
| 36 | |
| 37 | ### SwiftUI Implementation |
| 38 | |
| 39 | |
| 40 | TextField("Email", text: $email) |
| 41 | .keyboardType(.emailAddress) |
| 42 | .textContentType(.emailAddress) |
| 43 | .autocapitalization(.none) |
| 44 | .disableAutocorrection(true) |
| 45 | |
| 46 | |
| 47 | ### UIKit Implementation |
| 48 | |
| 49 | |
| 50 | textField.keyboardType = .emailAddress |
| 51 | textField.textContentType = .emailAddress |
| 52 | textField.autocapitalizationType = .none |
| 53 | textField.autocorrectionType = .no |
| 54 | |
| 55 | |
| 56 | |
| 57 | |
| 58 | ## Text Content Types |
| 59 | |
| 60 | Enable autofill by specifying content types: |
| 61 | |
| 62 | | Content | textContentType | Enables | |
| 63 | |---------|-----------------|---------| |
| 64 | | Name | `.name` | Contact autofill | |
| 65 | | Given name | `.givenName` | | |
| 66 | | Family name | `.familyName` | | |
| 67 | | Email | `.emailAddress` | Email autofill | |
| 68 | | Phone | `.telephoneNumber` | Phone autofill | |
| 69 | | Address | `.streetAddressLine1` | Address autofill | |
| 70 | | City | `.addressCity` | | |
| 71 | | State | `.addressState` | | |
| 72 | | ZIP | `.postalCode` | | |
| 73 | | Country | `.countryName` | | |
| 74 | | Credit card | `.creditCardNumber` | Camera card scanning | |
| 75 | | Username | `.username` | Keychain autofill | |
| 76 | | Password | `.password` | Keychain autofill | |
| 77 | | New password | `.newPassword` | Password generation | |
| 78 | | One-time code | `.oneTimeCode` | SMS autofill | |
| 79 | |
| 80 | ### Password and Login Fields |
| 81 | |
| 82 | |
| 83 | // Login form |
| 84 | TextField("Email", text: $email) |
| 85 | .textContentType(.username) |
| 86 | .keyboardType(.emailAddress) |
| 87 | |
| 88 | SecureField("Password", text: $password) |
| 89 | .textContentType(.password) |
| 90 | |
| 91 | // Registration form |
| 92 | SecureField("Create Password", text: $newPassword) |
| 93 | .textContentType(.newPassword) |
| 94 | |
| 95 | |
| 96 | ### One-Time Code (2FA) |
| 97 | |
| 98 | |
| 99 | TextField("Verification Code", text: $code) |
| 100 | .textContentType(.oneTimeCode) |
| 101 | .keyboardType(.numberPad) |
| 102 | |
| 103 | |
| 104 | iOS automatically suggests codes from SMS messages when this content type is set. |
| 105 | |
| 106 | |
| 107 | |
| 108 | ## Input Accessory Views |
| 109 | |
| 110 | ### When to Use |
| 111 | |
| 112 | Input accessory views appear above the keyboard for: |
| 113 | Navigation between fields (Previous/Next) |
| 114 | Custom actions (Done, formatting buttons) |
| 115 | Context-specific tools |
| 116 | |
| 117 | ### Standard Toolbar Pattern |
| 118 | |
| 119 | |
| 120 | struct FormTextField: View { |
| 121 | @Binding var text: String |
| 122 | @FocusState private var isFocused: Bool |
| 123 | |
| 124 | var body: some View { |
| 125 | TextField("Value", text: $text) |
| 126 | .focused($isFocused) |
| 127 | .toolbar { |
| 128 | ToolbarItemGroup(placement: .keyboard) { |
| 129 | Spacer() |
| 130 | Button("Done") { |
| 131 | isFocused = false |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | |
| 139 | ### Multi-Field Navigation |
| 140 | |
| 141 | |
| 142 | struct FormView: View { |
| 143 | @FocusState private var focusedField: Field? |
| 144 | |
| 145 | enum Field { |
| 146 | case firstName, lastName, email |
| 147 | } |
| 148 | |
| 149 | var body: some View { |
| 150 | Form { |
| 151 | TextField("First Name", text: $firstName) |
| 152 | .focused($focusedField, equals: .firstName) |
| 153 | |
| 154 | TextField("Last Name", text: $lastName) |
| 155 | .focused($focusedField, equals: .lastName) |
| 156 | |
| 157 | TextField("Email", text: $email) |
| 158 | .focused($focusedField, equals: .email) |
| 159 | } |
| 160 | .toolbar { |
| 161 | ToolbarItemGroup(placement: .keyboard) { |
| 162 | Button(action: focusPrevious) { |
| 163 | Image(systemName: "chevron.up") |
| 164 | } |
| 165 | .disabled(!canFocusPrevious) |
| 166 | |
| 167 | Button(action: focusNext) { |
| 168 | Image(systemName: "chevron.down") |
| 169 | } |
| 170 | .disabled(!canFocusNext) |
| 171 | |
| 172 | Spacer() |
| 173 | |
| 174 | Button("Done") { |
| 175 | focusedField = nil |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | |
| 183 | |
| 184 | |
| 185 | ## Keyboard Avoidance |
| 186 | |
| 187 | ### Automatic Behavior (SwiftUI) |
| 188 | |
| 189 | SwiftUI automatically adjusts for keyboard in most cases: |
| 190 | ScrollViews scroll to keep focused field visible |
| 191 | Safe area adjusts for keyboard height |
| 192 | |
| 193 | ### Manual Keyboard Handling |
| 194 | |
| 195 | |
| 196 | struct KeyboardAdaptive: ViewModifier { |
| 197 | @State private var keyboardHeight: CGFloat = 0 |
| 198 | |
| 199 | func body(content: Content) -> some View { |
| 200 | content |
| 201 | .padding(.bottom, keyboardHeight) |
| 202 | .onReceive(Publishers.keyboardHeight) { height in |
| 203 | withAnimation(.easeOut(duration: 0.25)) { |
| 204 | keyboardHeight = height |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | |
| 211 | ### Best Practices |
| 212 | |
| 213 | **Scroll to show field** - Focused field should be visible |
| 214 | **Don't cover important actions** - Submit buttons should remain accessible |
| 215 | **Animate adjustments** - Match keyboard animation (0.25s ease-out) |
| 216 | **Test on different devices** - Keyboard heights vary |
| 217 | |
| 218 | |
| 219 | |
| 220 | ## Hardware Keyboard Support |
| 221 | |
| 222 | ### Why It Matters |
| 223 | |
| 224 | iPads commonly use hardware keyboards. Apps should: |
| 225 | Support standard keyboard shortcuts |
| 226 | Provide discoverability |
| 227 | Not break when hardware keyboard is attached |
| 228 | |
| 229 | ### Standard Shortcuts to Support |
| 230 | |
| 231 | | Shortcut | Action | Notes | |
| 232 | |----------|--------|-------| |
| 233 | | ⌘N | New item | Standard creation | |
| 234 | | ⌘S | Save | | |
| 235 | | ⌘⌫ | Delete | With confirmation | |
| 236 | | ⌘F | Find/Search | | |
| 237 | | ⌘Z | Undo | | |
| 238 | | ⌘⇧Z | Redo | | |
| 239 | | ⌘C/⌘V/⌘X | Copy/Paste/Cut | | |
| 240 | | ⌘, | Settings/Preferences | | |
| 241 | | ⌘W | Close window/modal | | |
| 242 | | Escape | Cancel/dismiss | | |
| 243 | | Tab | Next field | | |
| 244 | | ⇧Tab | Previous field | | |
| 245 | | Return | Submit form | When appropriate | |
| 246 | |
| 247 | ### SwiftUI Keyboard Shortcuts |
| 248 | |
| 249 | |
| 250 | struct ContentView: View { |
| 251 | var body: some View { |
| 252 | NavigationStack { |
| 253 | ItemList() |
| 254 | .toolbar { |
| 255 | Button("New Item", action: createItem) |
| 256 | .keyboardShortcut("n", modifiers: .command) |
| 257 | } |
| 258 | } |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | // For custom shortcuts in list items |
| 263 | List { |
| 264 | ForEach(items) { item in |
| 265 | ItemRow(item: item) |
| 266 | } |
| 267 | } |
| 268 | .onDeleteCommand(perform: deleteSelected) // ⌘⌫ |
| 269 | |
| 270 | |
| 271 | ### Keyboard Shortcut Discoverability |
| 272 | |
| 273 | Hold ⌘ to show available shortcuts. Ensure your shortcuts appear: |
| 274 | |
| 275 | |
| 276 | Button("Save", action: save) |
| 277 | .keyboardShortcut("s", modifiers: .command) |
| 278 | // Appears in keyboard shortcut overlay |
| 279 | |
| 280 | |
| 281 | |
| 282 | |
| 283 | ## Text Editing |
| 284 | |
| 285 | ### Autocapitalization |
| 286 | |
| 287 | | Style | Use For | |
| 288 | |-------|---------| |
| 289 | | `.sentences` | General text, messages | |
| 290 | | `.words` | Names, titles | |
| 291 | | `.allCharacters` | Codes, abbreviations | |
| 292 | | `.none` | Email, usernames, URLs | |
| 293 | |
| 294 | ### Autocorrection |
| 295 | |
| 296 | | Setting | Use For | |
| 297 | |---------|---------| |
| 298 | | Enabled (default) | Prose, messages | |
| 299 | | Disabled | Code, usernames, specific values | |
| 300 | |
| 301 | |
| 302 | TextField("Username", text: $username) |
| 303 | .autocapitalization(.none) |
| 304 | .disableAutocorrection(true) |
| 305 | |
| 306 | |
| 307 | ### Text Input Traits Summary |
| 308 | |
| 309 | |
| 310 | TextField("Email", text: $email) |
| 311 | .keyboardType(.emailAddress) // Keyboard layout |
| 312 | .textContentType(.emailAddress) // Autofill hint |
| 313 | .autocapitalization(.none) // No auto-caps |
| 314 | .disableAutocorrection(true) // No autocorrect |
| 315 | .textInputAutocapitalization(.never) // iOS 15+ |
| 316 | |
| 317 | |
| 318 | |
| 319 | |
| 320 | ## Secure Text Entry |
| 321 | |
| 322 | ### Password Fields |
| 323 | |
| 324 | |
| 325 | SecureField("Password", text: $password) |
| 326 | .textContentType(.password) |
| 327 | |
| 328 | |
| 329 | ### Show/Hide Toggle Pattern |
| 330 | |
| 331 | |
| 332 | struct PasswordField: View { |
| 333 | @Binding var password: String |
| 334 | @State private var isSecure = true |
| 335 | |
| 336 | var body: some View { |
| 337 | HStack { |
| 338 | if isSecure { |
| 339 | SecureField("Password", text: $password) |
| 340 | } else { |
| 341 | TextField("Password", text: $password) |
| 342 | } |
| 343 | |
| 344 | Button(action: { isSecure.toggle() }) { |
| 345 | Image(systemName: isSecure ? "eye.slash" : "eye") |
| 346 | .foregroundColor(.secondary) |
| 347 | } |
| 348 | } |
| 349 | .textContentType(.password) |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | |
| 354 | |
| 355 | |
| 356 | ## Search Input |
| 357 | |
| 358 | ### Search Field Behavior |
| 359 | |
| 360 | |
| 361 | struct SearchView: View { |
| 362 | @State private var searchText = "" |
| 363 | |
| 364 | var body: some View { |
| 365 | NavigationStack { |
| 366 | List(filteredItems) { item in |
| 367 | ItemRow(item: item) |
| 368 | } |
| 369 | .searchable( |
| 370 | text: $searchText, |
| 371 | placement: .navigationBarDrawer(displayMode: .always), |
| 372 | prompt: "Search items" |
| 373 | ) |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | |
| 379 | ### Search Suggestions |
| 380 | |
| 381 | |
| 382 | .searchable(text: $searchText) { |
| 383 | ForEach(suggestions) { suggestion in |
| 384 | Text(suggestion.name) |
| 385 | .searchCompletion(suggestion.name) |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | |
| 390 | |
| 391 | |
| 392 | ## Common Patterns |
| 393 | |
| 394 | ### Form with All Best Practices |
| 395 | |
| 396 | |
| 397 | struct RegistrationForm: View { |
| 398 | @State private var email = "" |
| 399 | @State private var password = "" |
| 400 | @State private var confirmPassword = "" |
| 401 | @FocusState private var focusedField: Field? |
| 402 | |
| 403 | enum Field: Hashable { |
| 404 | case email, password, confirmPassword |
| 405 | } |
| 406 | |
| 407 | var body: some View { |
| 408 | Form { |
| 409 | Section("Account") { |
| 410 | TextField("Email", text: $email) |
| 411 | .keyboardType(.emailAddress) |
| 412 | .textContentType(.username) |
| 413 | .autocapitalization(.none) |
| 414 | .disableAutocorrection(true) |
| 415 | .focused($focusedField, equals: .email) |
| 416 | .submitLabel(.next) |
| 417 | .onSubmit { focusedField = .password } |
| 418 | |
| 419 | SecureField("Password", text: $password) |
| 420 | .textContentType(.newPassword) |
| 421 | .focused($focusedField, equals: .password) |
| 422 | .submitLabel(.next) |
| 423 | .onSubmit { focusedField = .confirmPassword } |
| 424 | |
| 425 | SecureField("Confirm Password", text: $confirmPassword) |
| 426 | .textContentType(.newPassword) |
| 427 | .focused($focusedField, equals: .confirmPassword) |
| 428 | .submitLabel(.done) |
| 429 | .onSubmit(register) |
| 430 | } |
| 431 | |
| 432 | Section { |
| 433 | Button("Create Account", action: register) |
| 434 | } |
| 435 | } |
| 436 | .toolbar { |
| 437 | ToolbarItemGroup(placement: .keyboard) { |
| 438 | Button("Previous") { moveFocus(-.previous) } |
| 439 | Button("Next") { moveFocus(.next) } |
| 440 | Spacer() |
| 441 | Button("Done") { focusedField = nil } |
| 442 | } |
| 443 | } |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | |
| 448 | ### Submit Label Options |
| 449 | |
| 450 | | Label | Use For | |
| 451 | |-------|---------| |
| 452 | | `.done` | Final field, closes keyboard | |
| 453 | | `.go` | Triggers action (search, navigate) | |
| 454 | | `.next` | Moves to next field | |
| 455 | | `.return` | Inserts newline (text areas) | |
| 456 | | `.search` | Search field | |
| 457 | | `.send` | Message composition | |
| 458 | | `.continue` | Multi-step forms | |
| 459 | | `.join` | Joining/connecting | |
| 460 | | `.route` | Navigation apps | |
| 461 | |
| 462 | |
| 463 | TextField("Search", text: $query) |
| 464 | .submitLabel(.search) |
| 465 | .onSubmit { performSearch() } |
| 466 | |
| 467 |
Details
- Updated
- Sep 10, 2026
- License
- MIT
- Stars on the repo
- ★ 2,235
- Checked
- Source
- wondelai/skills ↗
- Creator
- wondelai · 319 listings
Similar skills
See all →
SkillCross-Platform Reverse Engineer
SkillBrainstorm Ideas
SkillCode Review Feedback Analyzer
SkillBug Diagnosis Framework
SkillImplement Spec
SkillPR Description WriterMore from wondelai
SkillSoftware Design Philosophy
SkillActionable Metrics Guide
SkillAdvertising Media
SkillAffordance Design
SkillLean UX in Agile Teams
SkillAMP Framework Practitioner