Home > database >  SwiftUI change SecureField masking character
SwiftUI change SecureField masking character

Time:10-25

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

CodePudding user response:

You can do it with a proxy

import SwiftUI
//Shows a sample use
struct SecureParentView: View{
    @State var text: String = "secure"
    var body: some View{
        VStack{
            Text(text)
            MySecureFieldView(text: $text)
        }
    }
}
//The custom field
struct MySecureFieldView: View {
    @Binding var text: String
    //The proxy handles the masking
    var proxy: Binding<String>{
        Binding(get: {
            //Replace the return value
            var result = ""
            for _ in 0..<text.count{
                let char: Character = "\u{272A}"
                result.append(char)
            }
            return result
        }, set: { value in
            if value.isEmpty{
                text = value
            } else if value.count < text.count{
                //If the user deletes more than 1 character
                if text.count - value.count == 1{
                    text.removeLast()
                }else{
                    text = ""
                }
            } else if value.count > text.count{
                //If the user adds more than 1 character
                if value.count - text.count == 1{
                    let new = (value.last?.description) ?? ""
                    text.append(contentsOf: new)
                }else{
                    text = ""
                }
            }
        })
    }
    var body: some View {
        TextField("test", text: proxy)
    }
}

struct SecureParentView_Previews: PreviewProvider {
    static var previews: some View {
        SecureParentView()
    }
}

enter image description here

There is a slightly better solution for iOS 15, MacCatalyst 15 and macOS 12.

//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: {
            //Replace the return value
            var result = ""
            for _ in 0..<text.count{
                let char: Character = "\u{272A}"
                result.append(char)
            }
            return result
        }, 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)
            .overlay(
                ZStack{
                    //This is the regular textfield to hold the data
                    TextField("actual", text: $text)
                        .foregroundColor(Color(UIColor.clear))
                        .focused($focusedField, equals: 1)
                    //This will sit on top and is the only one that has color
                    //It will reduce in size to match lettering
                    Text(proxy.wrappedValue)
                        .minimumScaleFactor(0.2)
                        .foregroundColor(Color(UIColor.label))
                }
            )
            .foregroundColor(Color(UIColor.clear))
            .textSelection(.disabled)//You likely dont want this
            .onTapGesture {
                focusedField = 1
            }
    }
    
}

enter image description here

  • Related