SwiftUI change SecureField masking character

Viewed 355

SwiftUI on MacOS 11

The objective is to have a SwiftUI SecureField display a Unicode character different than the default bullets (••••••), for example an emoji, a randomly generated character, etc. An important part of the objective is that the actual text input by the user is fully editable and preserved, and accessible in a @State variable, and the masking characters are only displayed, but I don't mind if it's achieved with a TextField or some other View instead.

For example, the vanilla SecureField bullets:

struct ContentView : View {

  @State var password : String = ""

  var body: some View {

    VStack {
      SecureField("Password", text: $password)
      Button("Transmogrify!") {}
    }.padding()

  }
}

which results in this:

standard SecureField output

The objective is to achieve the same behaviour as a SecureField, but displaying a different character like this:

modified SecureField output

So far I have not been able to come up with a working code example. I have tried using a plain TextField in conjunction with an explicit Binding<String> to attempt to control the underlying text get/set but due to the nature of bindings that affects the text ultimately stored in password

1 Answers

You can do it with a proxy

import SwiftUI
//Shows a sample use
@available(iOS 15.0, macOS 12.0, *)
struct SecureParentView: View{
    @State var text: String = "secure"
    var body: some View{
        VStack{
            Text(text)
            MySecureFieldView(text: $text)
        }
    }
}
//The custom field
@available(iOS 15.0, macOS 12.0, *)
struct MySecureFieldView: View {
    @Binding var text: String
    //The proxy handles the masking
    var proxy: Binding<String>{
        Binding(get: {
            return text.map { _ in "\u{272A}" }.joined() 
        }, set: { value in
            //Not needed here because the TextField is focused
        })
    }
    @FocusState private var focusedField: Int?
    var body: some View {
        //This is for size. The 3 layers have to match so the cursor doesn't look off
        Text(text).lineLimit(1)
            .opacity(0)
        //Put a true secure field on top that has invisible text/dots
            .overlay(
                SecureField("actual", text: $text)
                    .foregroundColor(Color.clear)
                    .focused($focusedField, equals: 1)
                    .minimumScaleFactor(0.2)
                
            )
            .background(
                //This will sit on below but will look like it is on top
                //It will reduce in size to match lettering
                Text(proxy.wrappedValue)
                    .lineLimit(1)
                    .minimumScaleFactor(0.2)
                    .foregroundColor(Color(UIColor.label))
                    .opacity(1)
            )
            .foregroundColor(Color(UIColor.clear))
            .onTapGesture {
                focusedField = 1
            }
    }
    
}

@available(iOS 15.0, macOS 12.0, *)
struct SecureParentView_Previews: PreviewProvider {
    static var previews: some View {
        SecureParentView()
    }
}

enter image description here

Related