0

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.

chasepeeler
  • 137
  • 8
  • 2
    Does this answer your question? [How do I programmatically set secure text field and normal text field in swiftUI](https://stackoverflow.com/questions/71214283/how-do-i-programmatically-set-secure-text-field-and-normal-text-field-in-swiftui) – lorem ipsum Jan 26 '23 at 01:35
  • Yes and no. I do believe it shows me a better way to implement what I'm trying to do. I'm still curious if there is a way to do it along the path I was headed though, just for educational value. – chasepeeler Jan 26 '23 at 01:43
  • Actually, I don't think it will work. In that answer they don't have any label on the TextField/SecureField. That's a good chunk of code that I was looking to not have to duplicate. I tried pulling that out to it's own property, but then I start getting other errors – chasepeeler Jan 26 '23 at 01:58
  • 1
    I see what you are trying to do but I don't think its s viable, how would you toggle between them? The way in the link is the simplest way, just create a `@ViewBuilder` variable with the common code for the `Label` – lorem ipsum Jan 26 '23 at 02:00
  • Okay, using the example you linked to as a guide, I think I got it refactored to my liking. https://pastebin.com/gU4iwUP4 – chasepeeler Jan 26 '23 at 02:18
  • 1
    Feel free to answer your own question (totally legal on Stack Overflow). – matt Jan 26 '23 at 02:28

1 Answers1

0

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)
    }
  }
}

enter image description here

chasepeeler
  • 137
  • 8
  • 1
    Looks good you can also add as a variable `@ViewBuilder var label: some View{ Text("API Token").bold().padding(EdgeInsets(top: 0, leading: 44, bottom: 0, trailing: 0)) }` – lorem ipsum Jan 26 '23 at 13:43
  • I'm pretty sure I tried that and it gave me errors. I'll give it another try though (as I'd rather do it that way than have a separate view just for that one item) in case I missed something the first time – chasepeeler Jan 26 '23 at 15:41
  • That worked. I think I was doing `@ViewBuilder var label : some View = { Text...` Is there a way to define the variable so I can specify it as the second parameter in the TextField call? e.g. `TextField(text: $text, label: label)` – chasepeeler Jan 26 '23 at 20:56