How to Build a Secure Text Field in SwiftUI
Use SecureField instead of TextField whenever you need to mask sensitive input like passwords. Pair it with .textContentType(.password) so iOS can offer Password AutoFill from the keyboard.
@State private var password = ""
var body: some View {
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
.textContentType(.password)
.submitLabel(.done)
.padding()
}
Full implementation
The example below builds a complete sign-in form: an email field, a password field with show/hide toggle, a confirm-password field for sign-up flows, and a disabled submit button until both fields pass basic validation. All fields use textContentType so iOS AutoFill works correctly, and VoiceOver labels are set explicitly for accessibility.
import SwiftUI
struct SecureFieldDemoView: View {
@State private var email = ""
@State private var password = ""
@State private var confirmPassword = ""
@State private var isPasswordVisible = false
@State private var isConfirmVisible = false
@FocusState private var focusedField: Field?
enum Field: Hashable { case email, password, confirm }
private var passwordsMatch: Bool {
!password.isEmpty && password == confirmPassword
}
private var isFormValid: Bool {
email.contains("@") && password.count >= 8 && passwordsMatch
}
var body: some View {
NavigationStack {
VStack(spacing: 16) {
// Email
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .email)
.submitLabel(.next)
.onSubmit { focusedField = .password }
.padding(12)
.background(.background, in: RoundedRectangle(cornerRadius: 10))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
)
// Password with show/hide
passwordField(
label: "Password",
text: $password,
isVisible: $isPasswordVisible,
contentType: .newPassword,
focus: .password,
nextFocus: .confirm
)
// Confirm password
passwordField(
label: "Confirm Password",
text: $confirmPassword,
isVisible: $isConfirmVisible,
contentType: .newPassword,
focus: .confirm,
nextFocus: nil
)
// Validation hint
if !confirmPassword.isEmpty && !passwordsMatch {
Label("Passwords don't match", systemImage: "exclamationmark.circle")
.foregroundStyle(.red)
.font(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
}
// Submit
Button("Create Account") { /* submit action */ }
.buttonStyle(.borderedProminent)
.controlSize(.large)
.frame(maxWidth: .infinity)
.disabled(!isFormValid)
}
.padding()
.navigationTitle("Sign Up")
}
}
@ViewBuilder
private func passwordField(
label: String,
text: Binding<String>,
isVisible: Binding<Bool>,
contentType: UITextContentType,
focus: Field,
nextFocus: Field?
) -> some View {
HStack {
Group {
if isVisible.wrappedValue {
TextField(label, text: text)
.accessibilityLabel(label)
} else {
SecureField(label, text: text)
.accessibilityLabel(label)
}
}
.textContentType(contentType)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: focus)
.submitLabel(nextFocus == nil ? .done : .next)
.onSubmit {
if let next = nextFocus { focusedField = next }
else { focusedField = nil }
}
Button {
isVisible.wrappedValue.toggle()
} label: {
Image(systemName: isVisible.wrappedValue ? "eye.slash" : "eye")
.foregroundStyle(.secondary)
}
.accessibilityLabel(isVisible.wrappedValue ? "Hide password" : "Show password")
}
.padding(12)
.background(.background, in: RoundedRectangle(cornerRadius: 10))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
)
}
}
#Preview {
SecureFieldDemoView()
}
How it works
-
SecureField("Password", text: $password)— SwiftUI's built-in masked field. It replaces characters with bullets automatically on all platforms. No extra configuration is needed for the masking itself; the OS handles it. -
.textContentType(.newPassword)— Tells iOS this is a new-password context, prompting it to offer a strong-password suggestion and to save it in the iCloud Keychain on submission. Use.passwordfor sign-in flows. -
Show/hide toggle with
@State private var isPasswordVisible— The helperpasswordFieldview swaps betweenSecureFieldand plainTextFieldbased on this boolean. Both branches share the same binding so no data is lost on the swap. -
@FocusState+.onSubmit— Focus advances automatically from Email → Password → Confirm when the user taps the keyboard's return key, providing the expected tabbing behavior without any UIKit workarounds. -
.disabled(!isFormValid)on the button — The computedisFormValidproperty checks both email format and that passwords match before enabling submission, preventing server round-trips for trivially invalid input.
Variants
PIN / passcode field (numeric only)
For a 6-digit PIN, constrain input to numbers and use a custom dot-display so the field looks native to your design system without showing the system keyboard's suggestion bar.
struct PinField: View {
@State private var pin = ""
private let maxDigits = 6
var body: some View {
VStack(spacing: 16) {
// Dot indicator
HStack(spacing: 12) {
ForEach(0..<maxDigits, id: \.self) { index in
Circle()
.fill(index < pin.count ? Color.accentColor : Color.secondary.opacity(0.3))
.frame(width: 14, height: 14)
}
}
SecureField("PIN", text: $pin)
.keyboardType(.numberPad)
.textContentType(.oneTimeCode) // triggers SMS auto-fill
.onChange(of: pin) { _, newValue in
pin = String(newValue.filter(\.isNumber).prefix(maxDigits))
}
.frame(width: 1, height: 1) // visually hidden; focus still works
.opacity(0.01)
}
}
}
Inline validation with .onChange
To show real-time strength feedback, attach .onChange(of: password) and compute a strength score (character classes, length). Display a ProgressView(value:) below the field. Because SecureField hides characters, the feedback should be based on the binding value, not on what the user sees — this is already the case with $password.
Common pitfalls
-
⚠️ iOS 16 and earlier:
SecureFieldexists back to iOS 13, but the@FocusStatemodifier requires iOS 15+. If you need iOS 15/16 support, the code compiles fine — but always test Password AutoFill on a real device running the minimum OS you target, as simulator behaviour differs. -
⚠️ Swapping SecureField ↔ TextField loses focus: When toggling visibility by replacing one view type with another, focus can silently drop. Always set
focusedFieldback after the toggle inside awithAnimationblock or useDispatchQueue.main.asyncif needed to avoid the timing issue. -
⚠️ Accessibility labels are not inherited automatically: When you place
SecureFieldinside anHStackwith other views (like the eye button), VoiceOver may read the surrounding container rather than the field label. Always add explicit.accessibilityLabel("Password")to both the field and the toggle button, as shown in the full implementation.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement secure text field in SwiftUI for iOS 17+. Use SecureField. Add a show/hide password toggle using an eye SF Symbol button. Apply .textContentType(.newPassword) and .textContentType(.password) appropriately for sign-up and sign-in contexts. Make it accessible (VoiceOver labels on both field and toggle button). Add a #Preview with realistic sample data.
In Soarias, paste this prompt during the Build phase after your screen mockup is locked — Claude Code will scaffold the field directly into your existing LoginView.swift without touching unrelated files.
Related
FAQ
Does this work on iOS 16?
SecureField is available from iOS 13 onwards, and @FocusState from iOS 15. All code on this page compiles and runs on iOS 16 without modification. The only iOS 17+ feature in the snippets is .onChange(of:) using the two-parameter closure form — on iOS 16 use the single-parameter form instead: .onChange(of: pin) { newValue in ... }.
How do I trigger iCloud Keychain strong-password suggestions?
.textContentType(.newPassword) to the SecureField in sign-up forms. iOS recognises this hint and displays a "Use Strong Password" banner above the keyboard. For login forms, use .textContentType(.password) paired with a .textContentType(.username) on the email field — iOS AutoFill only surfaces saved credentials when it can match both fields in the same form.
What's the UIKit equivalent?
UITextField with isSecureTextEntry = true. The show/hide toggle is built by toggling that property at runtime. textContentType is identical and comes from UITextContentType. SwiftUI's SecureField wraps this UIKit control under the hood on iOS, so behaviour is identical — you just get a much more concise API.
Last reviewed: 2026-05-11 by the Soarias team.