Mete
Mete

Reputation: 5625

SwiftUI SecureField: How to achieve the same character obscuring behaviour as in UIKit?

My problem is SecureField in SwiftUI doesn’t display characters input by the user for any time at all, it just directly shows the '•' symbol for each character as it's typed - whereas in UIKit, UITextField (with isSecureTextEntry = true) shows the latest character for a second before hiding it behind '•'.

UX testers at my company have requested I bring back the "old behaviour" - but this behaviour doesn't seem part of any public API.

Interestingly this goes for UITextField custom classes injected into SwiftUI using UIViewRepresentable too - they behave in the "SwiftUI way" described above. So there's some contextual behaviour modification going on in SwiftUI for all secure UITextField behaviour? I'd have to completely rewrite my SwiftUI form into a full UIViewController to get back the behaviour (modally pushed UIViewControllers with secure UITextFields do exhibit the desired behaviour.)

Is this a sort of sideline bug in SwiftUI? I see the same thing for SwiftUI in both iOS13 and 14. Anyone seen a workaround or solution?

-EDIT-

After @Asperi's great explanation below, I noticed that my UITextField custom classes injected into SwiftUI using UIViewRepresentable were forcing this behaviour by unnecessarily setting the text binding in the updateUIView call. Using a Coordinator only to deal with text logic fixed the problem for me when using this method.

Upvotes: 3

Views: 1195

Answers (1)

Asperi
Asperi

Reputation: 257653

The observed effect is due to immediate apply to bound string state and immediate react/rebuild of view.

To bring desired behavior beck we need to postpone somehow state update and thus give a chance for SecuredField/UITextField to update self without synchronisation with state.

Here is a demo of possible direction (it is not ideal, but a way to go). Tested with Xcode 12.1 / iOS 14.1.

demo

struct DemoSecureFieldView: View {
    @State private var password = "demo"

    var textBinding: Binding<String>  {
        Binding(get: { password },
            set: { value in
                // more logic can be added to delay _only_ if new symbol added,
                // and force apply if next symbol came fast
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
                    password = value
                }
            }
        )
    }
    var body: some View {
        VStack {
            SecureField("Placeholder", text: textBinding)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
        }.background(Color.pink)
    }
}

Upvotes: 2

Related Questions