Reputation: 69
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
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
Reputation: 1908
Switching between TextField and SecureField as proposed in other answers has some drawbacks, mainly:
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
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