Reputation: 521
I've looked through the forums but I'm seeing mixed answers especially ones from an old Xcode version.
I only decided to add this after already typing up the code I have in this:
How could I go about doing that? I was wanting the 'Eyeball' toggle implemented on the password field.
Upvotes: 51
Views: 36706
Reputation: 2426
Here's a simple implementation of a secure text field that allows toggling between a secure and normal text view using SwiftUI only.
import SwiftUI
enum FocusedField { case normal, secure }
struct CustomSecureField: View {
@State var isSecure: Bool = true {
willSet {
if newValue {
focus = .secure
} else {
focus = .normal
}
}
}
@State var text: String = ""
@FocusState var focus: FocusedField?
var body: some View {
Group {
ZStack(alignment: .trailing) {
if isSecure {
SecureField("Password", text: $text)
.focused($focus, equals: .secure)
.animation(nil, value: isSecure)
} else {
TextField("Password", text: $text)
.focused($focus, equals: .normal)
.animation(nil, value: isSecure)
.textContentType(.password)
}
Image(systemName: isSecure ? "eye.slash" : "eye")
.animation(nil, value: isSecure)
.onTapGesture {
// This is necessary to prevent the keyboard from animating with a jump effect.
withAnimation {
isSecure.toggle()
}
}
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12).fill(.gray.opacity(0.2))
)
.padding()
}
}
#Preview {
CustomSecureField()
}
Upvotes: 0
Reputation: 4778
Unfortunately, all native SwiftUI implementations have different issues. (tested in iOS 18)
looks like UIKit + SwiftUI is the best for now
struct SecureTextField: UIViewRepresentable {
let placeHolder: String
@Binding var text: String
@Binding var isSecure: Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.isSecureTextEntry = isSecure
textField.text = text
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.textContentType = .newPassword
textField.attributedPlaceholder = NSAttributedString(string: placeHolder, attributes: [NSAttributedString.Key.foregroundColor: UIColor.lightGray])
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.isSecureTextEntry = isSecure
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: SecureTextField
init(_ parent: SecureTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
usage example
@State private var password = ""
@State private var isSecureText = true
HStack {
SecureTextField(placeHolder: "Enter Password", text: $password, isSecure: $isSecureText)
.padding()
.frame(height: 44)
Button {
isSecureText.toggle()
}label: {
Image(systemName: isSecureText ? "eye" : "eye.slash")
.symbolRenderingMode(.monochrome)
.foregroundStyle(Color.gray)
.frame(width: 16, height: 16)
}
.padding()
}
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.stroke(Color(red: 0.78, green: 0.78, blue: 0.8), lineWidth: 1)
}
Upvotes: 0
Reputation: 2466
I really like Lorenzo Fiamingo's "solution" - hacky, but very cool. I want to answer questions and to explain it further and also to show you how this can be made safer.
Mohammad Reza Koohkan asked: "How did you find offset 32? what happens if it was not in that position"
This solution actually works by changing a undocumented property of TextField called: "isSecure". If offset is wrong and you assume wrong type on that offset, it will overwrite bytes at that offset and lead to undefined behavior.
You can calculate correct offset of the property by summing up sizes of all of the properties preceding it.
I wrote a function to calculate the offset dynamically for any property of a View and by also checking the type of property value (for some added safety):
extension View {
func propertyOffset<T>(_ label: String, ofType type: T.Type) -> Int? {
var offset = 0
for child in Mirror(reflecting: self).children {
if child.label == label && child.value is T {
return offset
}
offset += MemoryLayout.size(ofValue: child.value)
}
return nil
}
}
If property does not exist it will return nil.
Then you can toggle value like this if found:
extension TextField {
public func secure(_ secure: Bool = true) -> TextField {
var secureField = self
if let offset = secureField.propertyOffset("isSecure", ofType: Bool.self) {
withUnsafeMutablePointer(to: &secureField) { pointer in
let valuePointer = UnsafeMutableRawPointer(mutating: pointer)
.assumingMemoryBound(to: Bool.self)
.advanced(by: offset)
valuePointer.pointee = secure
}
}
return secureField
}
}
Or just a bit simpler, since we already verified this is Bool type, you can also directly modify byte at offset:
extension TextField {
public func secure(_ secure: Bool = true) -> TextField {
var secureField = self
if let offset = secureField.propertyOffset("isSecure", ofType: Bool.self) {
withUnsafeMutableBytes(of: &secureField) { bytes in
bytes[offset] = secure ? 1 : 0
}
}
return secureField
}
}
Sentry.co commented: "Will not work if TextField has axis: .vertical":
It won't. You will notice SecureField does not have axis parameter. Secure fields are handling data bit differently internally (they contain only single character) so I guess this won't work, its not due to wrong offset of the modified field.
You can inspect list of all available properties at runtime like this:
extension View {
public func properties() -> Self {
for field in Mirror(reflecting: self).children {
print(field.label ?? "")
print(type(of: field.value))
print("----")
}
return self
}
}
Just use this like a modifier on any SwiftUI view, it will print out its properties to console at runtime.
Upvotes: 1
Reputation: 4069
Crazy (AKA don't use in production) and very breakable solution here (but working at the time of writing):
extension TextField {
public func secure(_ secure: Bool = true) -> TextField {
if secure {
var secureField = self
withUnsafeMutablePointer(to: &secureField) { pointer in
let offset = 32
let valuePointer = UnsafeMutableRawPointer(mutating: pointer)
.assumingMemoryBound(to: Bool.self)
.advanced(by: offset)
valuePointer.pointee = true
}
return secureField
} else {
return self
}
}
}
Usage
@State securing = true
...
TextField(...)
.secure(securing)
Upvotes: 2
Reputation: 747
With the help of this post, I created my own custom Password TextField where you can toggle between secure and unsecure mode, that keeps the cursor on at all times, and doesn't make the keyboard bounce
struct CustomPasswordTextField: UIViewRepresentable {
// MARK: Lifecycle
@Binding var text: String
let placeholder: String
@Binding var isSecureEntry: Bool
init(text: Binding<String>, placeholder: String, isSecureEntry: Binding<Bool>) {
_text = text
_isSecureEntry = isSecureEntry
self.placeholder = placeholder
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
let attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSAttributedString.Key.foregroundColor: UIColor.placeholderText])
setText(textField: textField, text: text, coordinator: context.coordinator)
textField.delegate = context.coordinator
textField.attributedPlaceholder = attributedPlaceholder
textField.textColor = .black
textField.autocapitalizationType = .none
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.placeholder = placeholder
setText(textField: uiView, text: text, coordinator: context.coordinator)
}
func makeCoordinator() -> Coordinator {
Coordinator(self, text: $text)
}
// MARK: Internal
class Coordinator: NSObject, UITextFieldDelegate {
// MARK: Lifecycle
init(_ parent: CustomPasswordTextField, text: Binding<String>) {
self.parent = parent
_text = text
}
// MARK: Internal
let parent: CustomPasswordTextField
@Binding var text: String
var updateCharacterTask: Task<Void, Never>?
// MARK: - UITextFieldDelegate
func 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 textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
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(nanoseconds: 800_000_000)
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
}
}
}
Upvotes: 0
Reputation: 533
The solution provided by Jonathan and others works great, except for one minor issue with the Autofill feature. After you interact with “eye” button, the “Save Password” prompt stop working, so you can use old hack with 0.001 opacity
ZStack {
TextField(placeholder, text: $text)
.focused($fieldToFocus, equals: .text)
.opacity(shouldShowTextField ? 1 : 0.001)
SecureField(placeholder, text: $text)
.textContentType(.password)
.focused($fieldToFocus, equals: .secure)
.opacity(shouldShowTextField ? 0.001 : 1)
}
The reason behind that setting opacity to zero remove View from View Tree, and we don’t want to do it here.
Please, not that only SecureField marked as .password
for textContentType
Upvotes: 0
Reputation: 131
This solution is suitable for iOS 14 and higher.
inputValue - Is a real input which a user enter
visibleInput - A user sees it in the text field
import SwiftUI
struct SecureInputView: View {
@Binding var inputValue: String
@State private var visibleInput: String = ""
@State private var isSecured = true
var body: some View {
ZStack(alignment: .trailing) {
TextField("Password", text: $visibleInput)
.onChange(of: visibleInput) { newValue in
guard isSecured else { inputValue = newValue; return }
if newValue.count >= inputValue.count {
let newItem = newValue.filter { $0 != Character("●") }
inputValue.append(newItem)
} else {
inputValue.removeLast()
}
visibleInput = String(newValue.map { _ in Character("●") })
}
Button {
isSecured.toggle()
visibleInput = isSecured ? String(inputValue.map { _ in Character("●") }) : inputValue
} label: {
(isSecured ? Image(systemName: "eye") : Image(systemName: "eye.slash"))
.tint(.gray)
}
}
}
}
Usage example:
struct ContentView: View {
@State private var password = ""
var body: some View {
SecureInputView(inputValue: $password)
.padding()
}
}
Upvotes: 3
Reputation: 55544
Here is a solution that meets the following requirements.
Requires iOS 15+, tested on iOS 15.0 (simulator) and 16.4 (device).
struct PasswordField: View {
let placeholder: String
@Binding
var text: String
@State
private var showText: Bool = false
private enum Focus {
case secure, text
}
@FocusState
private var focus: Focus?
@Environment(\.scenePhase)
private var scenePhase
var body: some View {
HStack {
ZStack {
SecureField(placeholder, text: $text)
.focused($focus, equals: .secure)
.opacity(showText ? 0 : 1)
TextField(placeholder, text: $text)
.focused($focus, equals: .text)
.opacity(showText ? 1 : 0)
}
Button(action: {
showText.toggle()
}) {
Image(systemName: showText ? "eye.slash.fill" : "eye.fill")
}
}
.onChange(of: focus) { newValue in
// if the PasswordField is focused externally, then make sure the correct field is actually focused
if newValue != nil {
focus = showText ? .text : .secure
}
}
.onChange(of: scenePhase) { newValue in
if newValue != .active {
showText = false
}
}
.onChange(of: showText) { newValue in
if focus != nil { // Prevents stealing focus to this field if another field is focused, or nothing is focused
DispatchQueue.main.async { // Needed for general iOS 16 bug with focus
focus = newValue ? .text : .secure
}
}
}
}
}
It can be used like so, where the focus state for the form will work correctly.
struct LoginView: View {
private enum Focus {
case email, password
}
@FocusState
private var focus: Focus?
@State
private var email: String = ""
@State
private var password: String = ""
var body: some View {
VStack {
TextField("[email protected]", text: $email)
.focused($focus, equals: .email)
PasswordField(placeholder: "*****", text: $password)
.focused($focus, equals: .password)
}
.onSubmit {
if focus == .email {
focus = .password
} else if focus == .password {
// do login
}
}
}
}
Upvotes: 9
Reputation: 2762
I put everything into a TextFieldStyle
struct AppTextFieldStyle: TextFieldStyle {
@Environment(\.colorScheme) var colorScheme
var label: String = ""
@Binding var fieldValue: String
var placeholderText: String = ""
var systemImage: String?
var isPassword: Bool = false
@State var showPassword = false
@Binding var hasError: Bool
@Binding var validationMessage: String
func _body(configuration: TextField<Self._Label>) -> some View {
VStack(spacing: 5) {
HStack {
Text(label)
.foregroundColor(.black)
Spacer()
}
HStack {
HStack(alignment: .bottom, spacing: 0) {
if let systemImage {
Image(systemName: systemImage)
.font(.headline)
.foregroundColor(
Color.gray.opacity(0.7))
.padding(.trailing, 10)
}
HStack {
if isPassword && showPassword {
TextField(placeholderText, text: self.$fieldValue)
} else {
configuration
}
}
.frame(minHeight: 23) // for some reason the textfield is not same height as secure textfield
if isPassword {
Button(
action: {
showPassword.toggle()
},
label: {
Image(systemName: self.showPassword ? "eye.slash" : "eye")
.accentColor(.gray)
}
)
.font(.headline)
.foregroundColor(hasError ? .red : .gray.opacity(0.70))
.padding(.leading, 10)
}
}
.padding(.all, 10)
.background(colorScheme == .dark ?
LinearGradient(
gradient: AppGradients.DarkTextFieldGradient.getGradient(),
startPoint: .top,
endPoint: .bottom
) : nil)
.background(colorScheme != .dark ? Color.white : nil)
}
.cornerRadius(10)
// .shadow(color: .black.opacity(0.40), radius: 2, x: 1, y: 1)
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(
hasError ? .red : .gray.opacity(0.90), lineWidth: 1)
}
.padding(.top, 5)
.foregroundColor(.black)
if hasError {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.font(.headline)
Text(validationMessage)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.footnote)
}.foregroundColor(.red)
}
}
}
}
then I use it like so
TextField("test", text: self.$email)
.textFieldStyle(
AppTextFieldStyle(
label: "Email",
fieldValue: self.$email,
systemImage: "person.circle",
hasError: .constant(true),
validationMessage: .constant("Please enter your email")
)
)
.focused($focus, equals: .email)
.foregroundColor(Color(.label))
.textInputAutocapitalization(.never)
.privacySensitive()
.disableAutocorrection(true)
//
SecureField("password", text: self.$password)
.textFieldStyle(
AppTextFieldStyle(
label: "Password",
fieldValue: self.$password,
placeholderText: "password",
systemImage: "lock.circle",
isPassword: true,
hasError: .constant(false),
validationMessage: .constant("please enter your password")
)
)
.focused($focus, equals: .password)
.textInputAutocapitalization(.never)
.privacySensitive()
.disableAutocorrection(true)
btw the focus state is defined in the form view
enum FocusableField: Hashable {
case email
case password
}
@FocusState private var focus: FocusableField?
Upvotes: 1
Reputation: 291
For those still looking for a simple solution to this issue (requires iOS 15 for swiftUI 3):
With the new @FocusState
introduced in swiftUI 3, it's possible to keep focus and keyboard open while changing State.
By using the opacity
modifier instead of conditionally changing between SecureField and TextField, the focus can jump between the two without issues with the keyboard.
This allows you to toggle between revealing and hiding the password with the the eye button included in the ZStack.
import SwiftUI
struct SecureTextFieldWithReveal: View {
@FocusState var focus1: Bool
@FocusState var focus2: Bool
@State var showPassword: Bool = false
@State var text: String = ""
var body: some View {
HStack {
ZStack(alignment: .trailing) {
TextField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus1)
.opacity(showPassword ? 1 : 0)
SecureField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus2)
.opacity(showPassword ? 0 : 1)
Button(action: {
showPassword.toggle()
if showPassword { focus1 = true } else { focus2 = true }
}, label: {
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill").font(.system(size: 16, weight: .regular))
.padding()
})
}
}
}
}
This is the code in LoginModifier
:
import SwiftUI
struct LoginModifier: ViewModifier {
var borderColor: Color = Color.gray
func body(content: Content) -> some View {
content
.disableAutocorrection(true)
.autocapitalization(.none)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(borderColor, lineWidth: 1))
}
}
The only issue I've had with this method is that on regaining focus SecureField
will automatically clear any text already entered if you start typing. This seems to be a design choice by Apple.
Upvotes: 19
Reputation: 140
I am afraid most answers here fail to mention that switching from SecureField
to TextField
reduces security. SecureField
is essentially, per Apple documentation, simply a TextField
where user input is masked [1]. However, SecureField
also does one other job - it prevents using third-party keyboards (keyboard extensions) and thus protects user's security and privacy.
Ideal solution would be to have input field that is both "secure" and has mask()
/unmask()
methods. Unfortunately, the only advice I found is when you want to implement unmasking as other answers suggested, at least block third-party keyboards from your application entirely [2]:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
return extensionPointIdentifier != UIApplication.ExtensionPointIdentifier.keyboard
}
}
@main
struct MyApplication: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Should also mention that UIApplicationDelegate
is part of UIKit, not SwiftUI. There is no "native" SwiftUI for the same purpose as for now, although the above works fine for now.
Upvotes: 8
Reputation: 257623
The possible approach is to show either TextField or SecureField joined to one storage, like in below demo:
with FocusState
, now it is possible to change fields without having the keyboard disappear
Main part:
if showPassword {
TextField("Placeholer", text: $password)
.focused($inFocus, equals: .plain)
} else {
SecureField("Placeholder", text: $password)
.focused($inFocus, equals: .secure)
}
Button("toggle") {
self.showPassword.toggle()
inFocus = showPassword ? .plain : .secure
}
Test module in project is here
struct DemoShowPassword: View {
@State private var showPassword: Bool = false
@State private var password = "demo"
var body: some View {
VStack {
if showPassword {
TextField("Placeholer", text: $password)
} else {
SecureField("Placeholder", text: $password)
}
Button("toggle") {
self.showPassword.toggle()
}
}
}
}
Upvotes: 17
Reputation: 3496
@State private var isPasswordVisible = false
ZStack {
TextField("", text: $password)
.opacity(isPasswordVisible ? 1 : 0)
SecureField("", text: $password)
.opacity(isPasswordVisible ? 0 : 1)
}
isPasswordVisible
Good Luck
Upvotes: 2
Reputation: 101
I've been looking for a nice solution for my use-case. I had to have an indicator which field is in focus. Successfully done that with onEditingChanged from TextField, but SecureField doesn't provide that closure. I tried stacking them both and disabling the SecureField so it only shows 'hidden' characters. That resulted in cursor sticking to the TextField text while SecureField text had different text width which made it seem buggy. Imagine a password with a lot of I's in it. The idea is to have a main binding with two side bindings that update the main one and sync each other.
struct CustomSecureField : View {
var label : String
@Binding var text : String
@State var isEditing = false
@State var isHidden = true
var body : some View {
let showPasswordBinding = Binding<String> {
self.text
} set: {
self.text = $0
}
let hidePasswordBinding = Binding<String> {
String.init(repeating: "●", count: self.text.count)
} set: { newValue in
if(newValue.count < self.text.count) {
self.text = ""
} else {
self.text.append(contentsOf: newValue.suffix(newValue.count - self.text.count) )
}
}
return ZStack(alignment: .trailing) {
TextField(
label,
text: isHidden ? hidePasswordBinding : showPasswordBinding,
onEditingChanged: { editingChanged in
isEditing = editingChanged
}
)
Image("eye").frame(width: 50, height: 50).onTapGesture {
isHidden.toggle()
}
}
}
}
}
Upvotes: 2
Reputation: 57
@Vahagn Gevorgyan's answer was almost correct but some people were struggling with maintaining state... this is because the field is using a binding which should ideally be held in a parent view. Therefore just update the bindings to state variables like this
struct SecureInputView: View {
let placeholder: String
@State var text: String
@State var isSecure: Bool = true
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecure {
SecureField(placeholder, text: $text)
} else {
TextField(placeholder, text: $text)
}
}.padding(.trailing, 32)
Button {
isSecure.toggle()
} label: {
Image(systemName: isSecure ? "lock.fill" : "lock.open")
}
}
}
}
Upvotes: 2
Reputation: 1
I made a custom text field that combine SecureField
and TextField
.
This is an example where I used my custom field for both email and pwd.
This is my solution:
struct CustomTextField: View {
let imageName: String
let placeholderText: String
var isSecureInput: Bool = false ///< define if this text field is secured and require eye button
@State private var isSecured: Bool
@Binding var text: String
init(image: String,
placeholder: String,
text: Binding<String>,
isSecureInput: Bool) {
imageName = image
placeholderText = placeholder
self._text = text
self.isSecureInput = isSecureInput
isSecured = isSecureInput
}
var body: some View {
VStack {
HStack {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
if isSecureInput {
Group {
if isSecured {
SecureField(placeholderText, text: $text)
}
else {
TextField(text, text: $text)
}
}
.disableAutocorrection(true)
.autocapitalization(.none)
.textContentType(.password)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
}
}
else {
TextField(placeholderText, text: $text)
}
}
Divider()
}
}
}
Upvotes: -1
Reputation: 2783
You can simply use this view instead of SecureField
. It has the eye icon inside, so for most cases you don't need to care about anything.
struct SecureInputView: View {
@Binding private var text: String
@State private var isSecured: Bool = true
private var title: String
init(_ title: String, text: Binding<String>) {
self.title = title
self._text = text
}
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecured {
SecureField(title, text: $text)
} else {
TextField(title, text: $text)
}
}.padding(.trailing, 32)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.accentColor(.gray)
}
}
}
}
Copy paste this view into your app, and instead of SecureField
just use SecureInputView
.
Example: SecureInputView("Password", text: $viewModel.password)
Upvotes: 59
Reputation: 118
I am using this approach for now in my current application. I would like to say that it works flawlessly.
@ViewBuilder
func secureField() -> some View {
if self.showPassword {
TextField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
} else {
SecureField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
}
}
Use:
HStack{
Image(systemName: "lock.fill")
.foregroundColor(passwordText.isEmpty ? .secondary : .primary)
.font(.system(size: 18, weight: .medium, design: .default))
.frame(width: 18, height: 18, alignment: .center)
secureField()
if !passwordText.isEmpty {
Button(action: {
self.showPassword.toggle()
}, label: {
ZStack(alignment: .trailing){
Color.clear
.frame(maxWidth: 29, maxHeight: 60, alignment: .center)
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill")
.font(.system(size: 18, weight: .medium))
.foregroundColor(Color.init(red: 160.0/255.0, green: 160.0/255.0, blue: 160.0/255.0))
}
})
}
}
.padding(.horizontal, 15)
.background(Color.primary.opacity(0.05).cornerRadius(10))
.padding(.horizontal, 15)
Upvotes: 2
Reputation: 460
@Derwrecked's answer really gave me some good inspirations: instead using two TextField, change SecureField
opacity and show/hide a Text
can avoid keyboard dismissing problem, but in his answer that long TouchDownUpEventModifier
seems unnecessarily complicated, you can easily achieve the same effect using a Button
with label.
So below is my approach, and the previews look like this
import SwiftUI
struct SecureInput: View {
let placeholder: String
@State private var showText: Bool = false
@State var text: String
var onCommit: (()->Void)?
var body: some View {
HStack {
ZStack {
SecureField(placeholder, text: $text, onCommit: {
onCommit?()
})
.opacity(showText ? 0 : 1)
if showText {
HStack {
Text(text)
.lineLimit(1)
Spacer()
}
}
}
Button(action: {
showText.toggle()
}, label: {
Image(systemName: showText ? "eye.slash.fill" : "eye.fill")
})
.accentColor(.secondary)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary, lineWidth: 1)
.foregroundColor(.clear))
}
}
struct SecureInput_Previews: PreviewProvider {
static var previews: some View {
Group {
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.previewLayout(.fixed(width: 400, height: 100))
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.preferredColorScheme(.dark)
.previewLayout(.fixed(width: 400, height: 100))
}
}
}
SecureField
has 0.0 opacity, so input cursor is not visible. But users can still keep typing without losing keyboard focus, so I find it acceptable, if anyone has a solution for this, please comment and share.Upvotes: 5
Reputation: 811
For those that do not want the keyboard disappearing while typing:
struct CustomSecureField: View {
@State var password: String = ""
@State var isShowingPassword: Bool = false
var body: some View {
VStack{
ZStack{
HStack{
SecureField(
isShowingPassword ? "" : "Password",
text: $password) {
}.opacity(isShowingPassword ? 0 : 1)
// show only one of these is not empty.
if(!password.isEmpty){
Image(systemName: isShowingPassword ? "eye.slash" : "eye")
.foregroundColor(.white)
.frame(width: 20, height: 20, alignment: .center)
.modifier(TouchDownUpEventModifier(changeState: { (buttonState) in
if buttonState == .pressed {
isShowingPassword = true
} else {
isShowingPassword = false
}
}))
}
}
if(isShowingPassword){
HStack{
Text(password)
.foregroundColor(.white)
.allowsHitTesting(false)
Spacer()
}
}
}
}.padding(10)
.background(Color.gray)
}
}
and the on tap and release modifier:
public enum ButtonState {
case pressed
case notPressed
}
/// ViewModifier allows us to get a view, then modify it and return it
public struct TouchDownUpEventModifier: ViewModifier {
/// Properties marked with `@GestureState` automatically resets when the gesture ends/is cancelled
/// for example, once the finger lifts up, this will reset to false
/// this functionality is handled inside the `.updating` modifier
@GestureState private var isPressed = false
/// this is the closure that will get passed around.
/// we will update the ButtonState every time your finger touches down or up.
let changeState: (ButtonState) -> Void
/// a required function for ViewModifier.
/// content is the body content of the caller view
public func body(content: Content) -> some View {
/// declare the drag gesture
let drag = DragGesture(minimumDistance: 0)
/// this is called whenever the gesture is happening
/// because we do this on a `DragGesture`, this is called when the finger is down
.updating($isPressed) { (value, gestureState, transaction) in
/// setting the gestureState will automatically set `$isPressed`
gestureState = true
}
return content
.gesture(drag) /// add the gesture
.onChange(of: isPressed, perform: { (pressed) in /// call `changeState` whenever the state changes
/// `onChange` is available in iOS 14 and higher.
if pressed {
self.changeState(.pressed)
} else {
self.changeState(.notPressed)
}
})
}
/// if you're on iPad Swift Playgrounds and you put all of this code in a seperate file,
/// you need to add a public init so that the compiler detects it.
public init(changeState: @escaping (ButtonState) -> Void) {
self.changeState = changeState
}
}
From what I have seen there is no easy way to keep the text showing unless you want to lose focus on your text.
Cheers!
Upvotes: 3