-
Notifications
You must be signed in to change notification settings - Fork 0
Book 09 Text And TextField
Part III — The User Interface · Claude's Xcode 26 Swift Bible
← Book-08-Lists-Grids-And-ForEach · Chapters and Appendices · Book-10-TextEditor-And-AttributedString →
QuickNote uses
TextFieldfor the note title andTextEditorfor the body, with the date picker, the photo grid, and the home-screen widget all hanging off that minimal text-input surface. Claudes LockBox usesTextFieldfor the vault item title and PIN entry, withkeyboardType: .numberPadon the PIN field and the standard alphanumeric keyboard on the title. Reading both side-by-side shows the same chapter's API in two different production contexts. Source: github.com/fluhartyml/QuickNote and github.com/fluhartyml/Claudes-LockBox. See Build-Along 02 (QuickNote) and Build-Along 03 (LockBox) for the build walkthroughs.
Text displays read-only strings in SwiftUI.
Text("Hello, world")
Text(verbatim: "Hello, world") // skips localization lookupUse verbatim: when you have a string that should never be localized (user-generated content, codes, etc.).
Standard Swift interpolation works inside Text:
Text("Score: \(score)")
Text("Price: \(price, format: .currency(code: "USD"))")
Text("Date: \(date, format: .dateTime.month().day())")
Text("Progress: \(value, format: .percent)")The format: parameter is the modern way. It handles localization automatically.
Text parses inline Markdown out of the box when you use a string literal:
Text("This is **bold** and this is *italic*")
Text("Visit [Apple](https://apple.com)")
Text("Use `code` formatting")
Text("~~Strikethrough~~ text")Watch out: Markdown parsing only works with string literals or LocalizedStringKey. If you pass a String variable, it renders as plain text:
let message = "This is **not bold**"
Text(message) // plain text, no markdown
// Fix: explicitly use LocalizedStringKey or AttributedString
Text(LocalizedStringKey(message)) // now it parses markdownYou can combine Text views with +:
Text("Name: ").bold() + Text(userName).foregroundStyle(.secondary)This is the only way to apply different styles to parts of the same line of text without using AttributedString.
Text("Title").font(.title)
Text("Body").font(.body)
Text("Caption").font(.caption)
Text("Custom").font(.system(size: 24, weight: .bold, design: .rounded))
Text("Custom").font(.system(.title, design: .monospaced))Built-in dynamic type sizes: .largeTitle, .title, .title2, .title3, .headline, .subheadline, .body, .callout, .footnote, .caption, .caption2.
Always prefer dynamic type over fixed sizes. It respects the user's accessibility settings.
Text("Styled").foregroundStyle(.primary)
Text("Tinted").foregroundStyle(.blue)
Text("Gradient").foregroundStyle(.linearGradient(
colors: [.blue, .purple],
startPoint: .leading,
endPoint: .trailing
))foregroundStyle replaced foregroundColor (now deprecated). Use foregroundStyle.
Text("Bold").bold()
Text("Italic").italic()
Text("Both").bold().italic()
Text("Light").fontWeight(.light)
Text("Heavy").fontWeight(.heavy)
Text("Wide").fontWidth(.expanded)Text("KERNING").kerning(2) // adjusts space between character pairs
Text("TRACKING").tracking(2) // uniform spacing added to every characterPractical difference: kerning respects ligatures and pair-specific adjustments. tracking is uniform. For most UI work, tracking is what you want. For body text, kerning is more typographically correct.
Watch out: You cannot use both on the same Text. If you apply both, kerning is ignored.
Text("Long text here...")
.lineLimit(2)
.truncationMode(.tail) // .head, .middle, .tail
Text("Flexible")
.lineLimit(1...5) // minimum 1 line, expand up to 5Text("Shifted").baselineOffset(5) // positive = up, negative = down
Text("Spaced").lineSpacing(8) // extra space between linesText("hello").textCase(.uppercase) // "HELLO"
Text("HELLO").textCase(.lowercase) // "hello"
Text("hello").textCase(nil) // no transformationMake text selectable by the user:
Text("Copy this").textSelection(.enabled)TextField is for single-line text input.
@State private var name = ""
TextField("Enter your name", text: $name)The first argument is the prompt/placeholder text.
For more control over the placeholder:
TextField(text: $name, prompt: Text("Full name").foregroundStyle(.tertiary)) {
Text("Name") // this is the label, used by accessibility
}A TextField can expand vertically with the axis parameter:
TextField("Write something", text: $bio, axis: .vertical)
.lineLimit(3...6) // starts at 3 lines, grows to 6This gives you a text field that behaves like a growing text area. It starts compact and expands as the user types. Combine with lineLimit to control the range.
Watch out: Without lineLimit, an axis: .vertical text field will grow without bound.
TextField("Email", text: $email)
.keyboardType(.emailAddress)
TextField("Phone", text: $phone)
.keyboardType(.phonePad)
TextField("Number", text: $amount)
.keyboardType(.decimalPad)Common types: .default, .asciiCapable, .numberPad, .decimalPad, .phonePad, .emailAddress, .URL, .webSearch.
TextField("Name", text: $name)
.textInputAutocapitalization(.words)Options: .never, .characters, .words, .sentences.
Watch out: .textInputAutocapitalization replaced the old .autocapitalization modifier. Use the new one.
TextField("Username", text: $username)
.autocorrectionDisabled(true)Tells the system what kind of data this field collects, enabling autofill:
TextField("Email", text: $email)
.textContentType(.emailAddress)
TextField("Street", text: $street)
.textContentType(.streetAddressLine1)Fires when the user taps Return:
TextField("Search", text: $query)
.onSubmit {
performSearch()
}You can also set the return key label:
TextField("Search", text: $query)
.submitLabel(.search) // .done, .go, .join, .next, .return, .search, .send
.onSubmit { performSearch() }Use format: for typed input that auto-formats:
@State private var amount: Double = 0
TextField("Amount", value: $amount, format: .currency(code: "USD"))
TextField("Percent", value: $percent, format: .percent)
TextField("Count", value: $count, format: .number)Watch out: The binding updates only when the user commits (presses Return), not on every keystroke.
For password entry. Characters are hidden.
@State private var password = ""
SecureField("Password", text: $password)
.textContentType(.password)
.onSubmit { authenticate() }SecureField supports the same modifiers as TextField: .onSubmit, .submitLabel, .textContentType, .textInputAutocapitalization, etc.
Manage which field has keyboard focus programmatically.
@FocusState private var isNameFocused: Bool
TextField("Name", text: $name)
.focused($isNameFocused)
Button("Focus Name") {
isNameFocused = true
}enum Field: Hashable {
case firstName, lastName, email
}
@FocusState private var focusedField: Field?
Form {
TextField("First", text: $first)
.focused($focusedField, equals: .firstName)
.onSubmit { focusedField = .lastName }
TextField("Last", text: $last)
.focused($focusedField, equals: .lastName)
.onSubmit { focusedField = .email }
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
.onSubmit { submit() }
}
.onAppear { focusedField = .firstName }This creates a tab-through flow: Return moves focus to the next field.
// Set focus to nil to dismiss keyboard
focusedField = nilWatch out: @FocusState must be Optional when used with an enum (the nil state means nothing is focused). The boolean variant is non-optional.
Watch out: Setting focus in onAppear can be unreliable on first launch. If you need guaranteed focus on appear, wrap it in a brief task delay:
.task {
try? await Task.sleep(for: .milliseconds(500))
focusedField = .firstName
}@State private var searchText = ""
@FocusState private var searchFocused: Bool
HStack {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField("Search", text: $searchText)
.focused($searchFocused)
.onSubmit { runSearch() }
.submitLabel(.search)
if !searchText.isEmpty {
Button {
searchText = ""
searchFocused = true
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
.padding(8)
.background(.quaternary, in: .rect(cornerRadius: 10))@State private var email = ""
@FocusState private var emailFocused: Bool
var emailIsValid: Bool {
email.contains("@") && email.contains(".")
}
TextField("Email", text: $email)
.focused($emailFocused)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.overlay(alignment: .trailing) {
if !email.isEmpty && !emailFocused {
Image(systemName: emailIsValid ? "checkmark.circle" : "exclamationmark.circle")
.foregroundStyle(emailIsValid ? .green : .red)
.padding(.trailing, 8)
}
}| Modifier | Purpose | |---|---| | .font() | Set text font | | .foregroundStyle() | Set text color/style | | .bold() / .italic() | Weight and emphasis | | .kerning() / .tracking() | Letter spacing | | .lineLimit() | Constrain line count | | .textCase() | Force upper/lower case | | .textSelection() | Enable copy | | .focused() | Bind to @FocusState | | .onSubmit {} | Handle Return key | | .submitLabel() | Return key appearance | | .keyboardType() | Keyboard layout | | .textInputAutocapitalization() | Cap behavior | | .textContentType() | Autofill hint | | .autocorrectionDisabled() | Disable autocorrect |
← Book-08-Lists-Grids-And-ForEach · Chapters and Appendices · Book-10-TextEditor-And-AttributedString →
Feedback: Found something off? Open an issue · Discuss it · Email Michael
Claude's X26 Swift6 Bible | GPL v3 | Built with Claude by Anthropic | Repo