sweta dodiya
sweta dodiya

Reputation: 69

toggle isSecureTextEntry in SwiftUI for SecureField

I wanted to implement feature to show and hide the password in SecureField. below is code for SecureField I had added button which will get detected to show and hide the text in SecureField. But swiftUI does not have feature similar to isSecureTextEntry Is there any other way instead of toggle between Textfield and SecureField here

SecureField(placeholder, text: $textValue, onCommit: {
    onReturn?()
})
.onTapGesture {
    onBegin?()
}
.keyboardType(keyboardType)
.font(Font.label.bodyDefault)
.disableAutocorrection(true)
.frame(maxWidth: .infinity, maxHeight: 40)
.padding(.leading)

Upvotes: 3

Views: 6783

Answers (3)

Yrb
Yrb

Reputation: 9675

This is not as straightforward as it should be. One of the answers there is pretty close, but it doesn't quite cover everything as it has a poor implementation of @FocusState. iOS 15 definitely made this field easier to construct, as there are two keys: 1. using .opacity() to show and hide the fields, 2. using @FocusState to make a seamless transition between the two fields. The only downside to this method is in order to make the transition seamless, you must restrict the keyboard to only ASCII-compatible characters, so some languages are left out, such as Russian.

The following code is updated to address the SecureField reset issues raised in the comments.

struct CustomSecureField: View {
    
    @FocusState private var focused: focusedField?
    @State private var showPassword: Bool = false
    @State private var internalPassword: String
    @State private var keepInternalPassword = false
    @Binding var password: String
    
    init(password: Binding<String>) {
        _internalPassword = State(initialValue: password.wrappedValue)
        _password = password
    }
    
    var body: some View {
        HStack {
            ZStack(alignment: .trailing) {
                TextField("Password", text: $internalPassword)
                    .focused($focused, equals: .unSecure)
                    .autocapitalization(.none)
                    .disableAutocorrection(true)
                // This is needed to remove suggestion bar, otherwise swapping between
                // fields will change keyboard height and be distracting to user.
                    .keyboardType(.alphabet)
                    .opacity(showPassword ? 1 : 0)
                SecureField("Password", text: $internalPassword)
                    .focused($focused, equals: .secure)
                    .autocapitalization(.none)
                    .disableAutocorrection(true)
                    .opacity(showPassword ? 0 : 1)
                Button(action: {
                    showPassword.toggle()
                    focused = focused == .secure ? .unSecure : .secure
                }, label: {
                    Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill")
                        .padding()
                })
                .onChange(of: focused, initial: false) { oldValue, newValue in
                    // This prevents sets the value for keepInternalPassword after a focus change
                    // Both values need to be compared because we need to insure the value was
                    // changed from .unsecure
                    if newValue == .secure && oldValue == .unSecure {
                        keepInternalPassword = true
                        print("focus onchange keepInternalPassword = true")
                    }
                }
                .onChange(of: internalPassword, initial: false) { oldValue, newValue in
                    // if the old value is being kept, the internal password is reset
                    // to that value here
                    if keepInternalPassword {
                        DispatchQueue.main.async {
                            keepInternalPassword = false
                            internalPassword = oldValue
                        }
                        return
                    }
                    // otherwise, update password
                    password = internalPassword
                }
            }
        }
    }
    // Using the enum makes the code clear as to what field is focused.
    enum focusedField {
        case secure, unSecure
    }
}

Upvotes: 6

Aleš Kocur
Aleš Kocur

Reputation: 1908

Switching between TextField and SecureField as proposed in other answers has some drawbacks, mainly:

  1. The keyboard dismisses when toggling between the states
  2. The field clears when you start typing after toggling between states

I created my own TextField using the UITextField and the UIViewRepresentable protocol and solved those two issues with it.

import SwiftUI

struct TextField_port: UIViewRepresentable {
    // MARK: Lifecycle

    init(text: Binding<String>, placeholder: String = "", isSecureEntry: Bool = false) {
        _text = text
        self.isSecureEntry = isSecureEntry
        self.placeholder = placeholder
    }

    // MARK: Internal

    typealias UIViewType = UITextField

    class Coordinator: NSObject, UITextFieldDelegate {
        // MARK: Lifecycle

        init(_ parent: TextField_port, text: Binding<String>) {
            self.parent = parent
            _text = text
        }

        // MARK: Internal

        let parent: TextField_port
        @Binding var text: String
        var updateCharacterTask: Task<Void, Never>?

        // MARK: - UITextFieldDelegate

        func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
            updateCharacterTask?.cancel()

            if let textRange = Range(range, in: text) {
                let updatedText = text.replacingCharacters(in: textRange,
                                                           with: string)
                text = updatedText
            }

            return false
        }

        func textFieldShouldClear(_ textField: UITextField) -> Bool {
            updateCharacterTask?.cancel()
            text = ""
            return false
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            textField.resignFirstResponder()
            return true
        }
    }

    @Binding var text: String
    let placeholder: String
    let isSecureEntry: Bool

    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        setText(textField: textField, text: text, coordinator: context.coordinator)
        textField.delegate = context.coordinator
        textField.placeholder = placeholder
        textField.returnKeyType = .done
        textField.clearButtonMode = .whileEditing
        textField.autocapitalizationType = .none

        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.placeholder = placeholder
        setText(textField: uiView, text: text, coordinator: context.coordinator)
    }

    private func setText(textField: UITextField, text: String, coordinator: Coordinator) {

        coordinator.updateCharacterTask?.cancel()

        if isSecureEntry {
            let securedText = text.map { _ in "●" }.joined()

            // If we are adding a new character, reveal last for a few seconds
            if (textField.text?.count ?? 0) < text.count {
                var lastRevealed = securedText
                lastRevealed.removeLast()
                textField.text = lastRevealed + text.suffix(1)

                // Hide last character after a delay
                coordinator.updateCharacterTask = Task { [securedText] in
                    try? await Task.sleep(seconds: 0.4)

                    if !Task.isCancelled {
                        await MainActor.run {
                            textField.text = securedText
                        }
                    }
                }

            } else {
                // If we are deleting characters, just show the secured text
                textField.text = securedText
            }
        } else {
            textField.text = text
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self, text: $text)
    }
}

struct TextField_port_Previews: PreviewProvider {

    @State static var text: String = ""

    static var previews: some View {
        TextField_port(text: $text, placeholder: "test")
    }
}

Upvotes: 4

Vahid
Vahid

Reputation: 3496

Accepted answer (Yrb answers) is not working properly, when switch the eye to visible pass and enter some password and again switch back to secure mode and enter some character, the field will be empty and all entered characters will be gone.

It's my solution that doesn't have any dependency on iOS 15 too:

struct CustomSecureField: View {

@State var isPasswordVisible: Bool = false
@Binding var password: String
var placeholder = ""

var body: some View {
    HStack(spacing: 12) {
        ZStack {
            if password.isEmpty {
                HStack {
                    Text(placeholder)
                    Spacer()
                }
            }
            
            ZStack {
                TextField("",
                          text: $password)
                .frame(maxHeight: .infinity)
                .opacity(isPasswordVisible ? 1 : 0)
                
                SecureField("",
                            text: $password)
                .frame(maxHeight: .infinity)
                .opacity(isPasswordVisible ? 0 : 1)
            }
        }
        .padding(.horizontal, 8)
        Button {
            isPasswordVisible.toggle()
        } label: {
            Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
        }
        .padding(.trailing, 8)
    }
    .frame(height: 44)
    .frame(maxWidth: .infinity)
    .background(Color.gray.opacity(0.4))
    .cornerRadius(5)
    .padding(.horizontal)
}

}

Upvotes: 2

Related Questions