swiftswiftuiswiftui-view

How to refactor almost identical conditional code in SwiftUI view


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.


Solution

  • 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