How to toggle Secure Input in SwiftUI TextField


Greetings, traveler!

SwiftUI provides a clean and declarative way to build user interfaces, but it doesn’t always include built-in solutions for every use case. One such limitation is the lack of a native way to toggle the secure input state of a text field, such as switching between a standard TextField and a SecureField for password inputs. In UIKit, you can simply use the isSecureTextEntry property on a UITextField to handle this. This is a common requirement for login screens where users might want to reveal or hide their password. In this article, we’ll explore a practical approach to address this by creating a custom SecureTextField view that seamlessly switches between secure and non-secure input modes.

The Challenge

In SwiftUI, TextField and SecureField are distinct views with different behaviors. TextField displays text as-is, while SecureField obscures it for sensitive data like passwords. There’s no built-in mechanism to toggle a single field between these states dynamically. To achieve this, we can create a custom view that manages both fields and switches their visibility based on a binding.

The Solution: A Custom SecureTextField

The SecureTextField view presented here encapsulates both a TextField and a SecureField, using a binding to control which one is active. It ensures a smooth user experience by maintaining focus and synchronizing the text input across both fields. Below is the implementation, followed by a breakdown of how it works.

import SwiftUI

public struct SecureTextField: View {
    private enum Field: Hashable {
        case regular, secure
    }
    
    @Binding private var text: String
    private let isSecureTextEntry: Bool
    
    @FocusState private var focusedField: Field?
    
    private let title: LocalizedStringKey
    private var textContentType: UITextContentType = .password
    
    public init(
        _ title: LocalizedStringKey,
        text: Binding<String>,
        isSecure: Bool
    ) {
        self.title = title
        _text = text
        isSecureTextEntry = isSecure
    }
    
    public var body: some View {
        Group {
            if isSecureTextEntry {
                secureField()
            } else {
                textField()
            }
        }
        .onChange(of: isSecureTextEntry) { _, newValue in
            focusedField = isSecureTextEntry ? .secure : .regular
        }
    }
}

private extension SecureTextField {
    func textField() -> some View {
        TextField(title, text: $text)
            .focused($focusedField, equals: .regular)
            .autocorrectionDisabled()
            .textContentType(textContentType)
    }
    
    func secureField() -> some View {
        SecureField(title, text: $text)
            .focused($focusedField, equals: .secure)
            .textContentType(textContentType)
    }
}

public extension SecureTextField {
    func setTextContentType( _ textContentType: UITextContentType) -> some View {
        var view = self
        view.textContentType = textContentType
        return view
    }
}

How It Works

The SecureTextField view is designed to be reusable and straightforward. Here’s a step-by-step explanation of its components and logic:

  1. State and Bindings:
    • @Binding var text: String: This binds to the text input, shared between both fields to ensure the entered text persists when switching modes.
    • let isSecureTextEntry: Bool: This controls whether the TextField (false) or SecureField (true) is active.
    • @FocusState private var focusedField: Field?: A private state to manage which field has focus, using an enum (Field) to differentiate between the regular and secure states.
  2. View Composition:
    • The textField() and secureField() helper methods define the TextField and SecureField, respectively. Each is bound to the same text binding and uses the focused modifier to manage focus state.
  3. Focus Management:
    • The .onChange(of: isSecureTextEntry) modifier updates the focusedField state whenever isSecureTextEntry changes, ensuring the correct field gains focus. This prevents the keyboard from dismissing during the switch, maintaining a smooth user experience.
  4. Additional Configurations:
    • autocorrectionDisabled() is applied to the TextField to avoid unwanted autocorrect behavior, especially for passwords.
    • The textContentType is set on both fields view builder method to ensure appropriate keyboard types and autofill behavior.

Example Usage

To demonstrate how SecureTextField can be used, here’s a sample implementation with a toggle button:

struct ContentView: View {
    @State private var text: String = ""
    @State private var isSecure: Bool = false
    
    var body: some View {
        ScrollView {
            VStack {
                SecureTextField(
                    "Password",
                    text: $text,
                    isSecureTextEntry: $isSecure
                )
                .padding()
                
                Button(isSecure ? "Show Password" : "Hide Password") {
                    isSecure.toggle()
                }
            }
            
            Spacer()
        }
    }
}

In this example, the SecureTextField is paired with a button that toggles isSecure. When the button is pressed, the view switches between secure and non-secure input, updating the visibility and focus of the fields while preserving the entered text.

Why This Approach?

This solution is lightweight and reusable, requiring minimal code to integrate into any SwiftUI project. By using a ZStack and opacity to switch between fields, it avoids complex state management or view recreation. The focus handling ensures the keyboard remains active during the switch, which is critical for usability. Additionally, the view supports localization and text content types, making it versatile for various input scenarios.

Conclusion

While SwiftUI doesn’t natively support toggling secure input for text fields, creating a custom view offers a clean and effective solution. By managing two fields with shared state and focus, this component provides a seamless user experience for password visibility toggling.