1

I'm having some difficulty implementing a custom textfield in SwiftUI. I'm trying to create a password field, however I'm having to dip into UIKit because I need to react when the field is focused, and (unlike with the standard TextField in SwiftUI) there is no onEditingChanged closure with SecureField.

So I have the following :

struct PasswordField: UIViewRepresentable {

    @ObservedObject var viewModel: TextFieldFloatingWithBorderViewModel
    
    func makeUIView(context: UIViewRepresentableContext<PasswordField>) -> UITextField {
        let tf = UITextField(frame: .zero)
        tf.isUserInteractionEnabled = true
        tf.delegate = context.coordinator
        return tf
    }

    func makeCoordinator() -> PasswordField.Coordinator {
        return Coordinator(viewModel: viewModel)
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = viewModel.text
        uiView.isSecureTextEntry = !viewModel.isRevealed
    }

    class Coordinator: NSObject, UITextFieldDelegate {
        @ObservedObject var viewModel: TextFieldFloatingWithBorderViewModel
        
        init(viewModel: TextFieldFloatingWithBorderViewModel) {
            self.viewModel = viewModel
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            DispatchQueue.main.async {
                self.viewModel.text = textField.text ?? ""
            }
        }

        func textFieldDidBeginEditing(_ textField: UITextField) {
            DispatchQueue.main.async {
                self.viewModel.isFocused = true
            }
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            DispatchQueue.main.async {
                self.viewModel.isFocused = false
            }
        }

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

We then declare this field as follows within a SwiftUI View:

HStack {
    PasswordField(viewModel: viewModel)
    Button(action: {
        viewModel.toggleReveal()
    }) {
        viewModel.revealIcon
            .foregroundColor(viewModel.isFocused ? .blue : viewModel.hasWarning ? .red: .gray)
    }
 }

In the TextFieldFloatingWithBorderViewModel viewModel we have a Published var isRevealed and then the following method called when the button is tapped:

func toggleReveal() {
    isRevealed.toggle()
}

Because in the PasswordField we have:

uiView.isSecureTextEntry = !viewModel.isRevealed

This toggles the password field between secure view (i.e. input masked with dots) and standard view. This is working well, except that when the user toggles back to hidden and continues to type the password is being wiped:

enter image description here

I cannot work out why the password is being wiped here, and only when it goes from non secure to secure (not the other way around)

DevB1
  • 1,235
  • 3
  • 17
  • 43
  • Perhaps it could be related to the fact that the swiftUI view gets destroyed and renewed when the view model changes, hence triggering again the makeUI method. Could you also add the code for `TextFieldFloatingWithBorderViewModel`? – valeCocoa Mar 09 '22 at 17:06
  • IMHO this is the standard behavior of a SecureField – once defocused if you restart typing it gets swiped. You'll get exactly the same behavior with a standard SecureField. – ChrisR Mar 09 '22 at 17:13
  • Why not stick to SwiftUI and use `.onChange(of: , perform: )`? This is not a difficult thing to set up. – Yrb Mar 09 '22 at 17:40
  • I'm trying to trigger an action (i.e. highlighting the field) when the user navigates to that field. I don't think there's anything I can put in the .onChange closure to catch this? – DevB1 Mar 09 '22 at 17:43

1 Answers1

0

You already have the @FocusState for this. You can use it like any state variable. Here is some code where I put a yellow background on a TextField and conditionally controlled it with the @FocusState variable:

struct HighlightWhenFocused: View {
    @State private var password: String = ""
    @FocusState var passwordFocused: Bool

    var body: some View {
        PasswordView(password: $password)
            .focused($passwordFocused)
            .background(
                Color.yellow
                    .opacity(passwordFocused ? 1 : 0)
            )
    }
}

struct PasswordView: View {
    
    @Binding var password: String
    @State private var secured = true
    
    var body: some View {
        HStack{
            Text("Password:")
            if secured{
                SecureField("",text:$password)
            }
            else{
                TextField("",text: $password)
            }
            Button {
                secured.toggle()
            } label: {
                Image(systemName: secured ? "eye.slash" : "eye")
            }
            .buttonStyle(BorderlessButtonStyle())
        }
    }
}

Edited it to make it a secure password solution. Please note that when you switch between viewable/non-viewable, the fields lose focus because you tapped outside of them.

Yrb
  • 8,103
  • 2
  • 14
  • 44