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:
The objective is to achieve the same behaviour as a SecureField, but displaying a different character like this:
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()
}
}
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
}
}
}