```html SwiftUI: How to Build a Secure Text Field (iOS 17+, 2026)

How to Build a Secure Text Field in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: SecureField Updated: May 11, 2026
TL;DR

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

  1. 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.
  2. .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 .password for sign-in flows.
  3. Show/hide toggle with @State private var isPasswordVisible — The helper passwordField view swaps between SecureField and plain TextField based on this boolean. Both branches share the same binding so no data is lost on the swap.
  4. @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.
  5. .disabled(!isFormValid) on the button — The computed isFormValid property 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

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?
Yes. 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?
Apply .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?
In UIKit the equivalent is 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.

```