I have the following that I'm using for an input field where the user can enter an API token. By default it's presented as a SecureField, but the user can click the "eye" icon and change to a regular field
struct PasswordField : View {
@Binding var value : String
@State var showToken: Bool = false
var body : some View {
if(showToken){
ZStack(alignment: .trailing) {
TextField(text: $value) {
Text("API Token").bold().padding(EdgeInsets(top: 0, leading: 44, bottom: 0, trailing: 0))
}
Image(systemName: "eye").onTapGesture {
showToken.toggle()
}.padding(.trailing,5)
}
} else {
ZStack(alignment: .trailing) {
SecureField(text: $value) {
Text("API Token").bold().padding(EdgeInsets(top: 0, leading: 44, bottom: 0, trailing: 0))
}
Image(systemName: "eye.slash").onTapGesture {
showToken.toggle()
}.padding(.trailing,5)
}
}
}
}
It works as I have written it, but I hate the fact I have almost the exact same code in the if and else blocks. The only differences are that one is TextField and the other SecureField, as well as the icon that is displayed, eye vs eye.slash. How can I refactor that in order to reduce code duplication?
I tried to create another View that just contained the duplicate code, but I didn't know how to specify whether to use TextField or SecureField. I figured I might be able to do so with Generics, and created the following
protocol MyProtocol {}
extension SecureField : MyProtocol {}
extension TextField: MyProtocol {}
struct InnerView<T> : View where T:MyProtocol {
@Binding var value : String
@Binding var showToken: Bool
var icon : String
var body : some View {
ZStack(alignment: .trailing) {
T(text: $value) {
Text("API Token").bold().padding(EdgeInsets(top: 0, leading: 44, bottom: 0, trailing: 0))
}
Image(systemName: icon).onTapGesture {
showToken.toggle()
}.padding(.trailing,5)
}
}
}
The problem with this is MyProtocol didn't have the proper init, so I added the same init that exists in TextField and SecureField
protocol MyProtocol {
init(text: Binding<String>, prompt: Text?, @ViewBuilder label: () -> Label)
}
but Label
is a generic itself, and that's where I hit a dead end.
I am rather new to Swift, but I do have a lot of programming experience. I come from the web development world using PHP, Javascript, etc. though.
Using the example in the answer linked to by lorem ipsum above, I came up with the following solution that is pretty much refactored to my liking
struct PasswordFieldLabel : View {
var body:some View {
Text("API Token").bold().padding(EdgeInsets(top: 0, leading: 44, bottom: 0, trailing: 0))
}
}
struct PasswordField : View {
@Binding var value : String
@State var showToken: Bool = false
var body : some View {
ZStack(alignment: .trailing) {
if(showToken){
TextField(text: $value){
PasswordFieldLabel()
}
} else {
SecureField(text: $value){
PasswordFieldLabel()
}
}
Button(action: {
showToken.toggle()
}, label: {
Image(systemName: showToken ? "eye" : "eye.slash")
}
).padding(.trailing, 7).buttonStyle(.plain)
}
}
}