2

I am trying to create an OTP page for my app but I don't know how to make the next textfield focus after I input a single digit in the first text field.

I created 6 text field for each digit of OTP. The next text field should be the first responder once I key in one digit from the first text field and so forth untill all 6 digits are complete.

I'm not sure how to do that in Swift UI. So far I manage to create 6 lines only as seen in the screenshot. The expected is only one digit should be per line. So the next text field should be focus once I input a single integer.

I tried other post like the use of @FocusState but it says unknown attribute.

I also tried the custom text field How to move to next TextField in SwiftUI? but I cannot seem to make it work.


import SwiftUI


struct ContentView: View {
    
    
    @State private var OTP1 = ""
    @State private var OTP2 = ""
    @State private var OTP3 = ""
    @State private var OTP4 = ""
    @State private var OTP5 = ""
    @State private var OTP6 = ""
    
    
    var body: some View {
        
        VStack {
            
            HStack(spacing: 16) {
                VStack {
                    TextField("", text: $OTP1)
                    Line()
                        .stroke(style: StrokeStyle(lineWidth: 1))
                        .frame(width: 41, height: 1)
                }
                
                VStack {
                    TextField("", text: $OTP2)
                    Line()
                        .stroke(style: StrokeStyle(lineWidth: 1))
                        .frame(width: 41, height: 1)
                }
                
                VStack {
                    TextField("", text: $OTP3)
                    Line()
                        .stroke(style: StrokeStyle(lineWidth: 1))
                        .frame(width: 41, height: 1)
                }
                
                VStack {
                    TextField("", text: $OTP4)
                    Line()
                        .stroke(style: StrokeStyle(lineWidth: 1))
                        .frame(width: 41, height: 1)
                }
                
                VStack {
                    TextField("", text: $OTP5)
                    Line()
                        .stroke(style: StrokeStyle(lineWidth: 1))
                        .frame(width: 41, height: 1)
                }
                
                VStack {
                    TextField("", text: $OTP6)
                    Line()
                        .stroke(style: StrokeStyle(lineWidth: 1))
                        .frame(width: 41, height: 1)
                }
            }
            
        }
    }
}

struct Line: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: rect.width, y: 0))
        return path
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewLayout(.fixed(width: 560, height: 50))
    }
}

My OTP Page

Expected field

Steve Vinoski
  • 19,572
  • 3
  • 28
  • 41
Dreiohc
  • 125
  • 1
  • 8
  • 1
    `@FocusState` was added in iOS 15. To do that I think you need Xcode beta. Is it a problem to update your project to support iOS 15? If it is you can achieve the same look but it will be a hassle. – Alhomaidhi Sep 02 '21 at 07:40
  • @Alhomaidhi unforunately, we still need to continue our xcode version till next year. – Dreiohc Sep 02 '21 at 08:58

1 Answers1

6

Here is my answer for iOS 14.

The view.


struct ContentView: View {
    
      @StateObject var viewModel = ViewModel()
      @State var isFocused = false
      
      let textBoxWidth = UIScreen.main.bounds.width / 8
      let textBoxHeight = UIScreen.main.bounds.width / 8
      let spaceBetweenBoxes: CGFloat = 10
      let paddingOfBox: CGFloat = 1
      var textFieldOriginalWidth: CGFloat {
          (textBoxWidth*6)+(spaceBetweenBoxes*3)+((paddingOfBox*2)*3)
      }
      
      var body: some View {
              
              VStack {
                  
                  ZStack {
                      
                      HStack (spacing: spaceBetweenBoxes){
                          
                          otpText(text: viewModel.otp1)
                          otpText(text: viewModel.otp2)
                          otpText(text: viewModel.otp3)
                          otpText(text: viewModel.otp4)
                          otpText(text: viewModel.otp5)
                          otpText(text: viewModel.otp6)
                      }
                      
                      
                      TextField("", text: $viewModel.otpField)
                      .frame(width: isFocused ? 0 : textFieldOriginalWidth, height: textBoxHeight)
                      .disabled(viewModel.isTextFieldDisabled)
                      .textContentType(.oneTimeCode)
                      .foregroundColor(.clear)
                      .accentColor(.clear)
                      .background(Color.clear)
                      .keyboardType(.numberPad)
                  }
          }
      }
      
      private func otpText(text: String) -> some View {
          
          return Text(text)
              .font(.title)
              .frame(width: textBoxWidth, height: textBoxHeight)
              .background(VStack{
                Spacer()
                RoundedRectangle(cornerRadius: 1)
                    .frame(height: 0.5)
               })
              .padding(paddingOfBox)
      }
}

This is the viewModel.

class ViewModel: ObservableObject {
    
    @Published var otpField = "" {
        didSet {
            guard otpField.count <= 6,
                  otpField.last?.isNumber ?? true else {
                otpField = oldValue
                return
            }
        }
    }
    var otp1: String {
        guard otpField.count >= 1 else {
            return ""
        }
        return String(Array(otpField)[0])
    }
    var otp2: String {
        guard otpField.count >= 2 else {
            return ""
        }
        return String(Array(otpField)[1])
    }
    var otp3: String {
        guard otpField.count >= 3 else {
            return ""
        }
        return String(Array(otpField)[2])
    }
    var otp4: String {
        guard otpField.count >= 4 else {
            return ""
        }
        return String(Array(otpField)[3])
    }
    
    var otp5: String {
        guard otpField.count >= 5 else {
            return ""
        }
        return String(Array(otpField)[4])
    }
    
    var otp6: String {
        guard otpField.count >= 6 else {
            return ""
        }
        return String(Array(otpField)[5])
    }
    
    @Published var borderColor: Color = .black
    @Published var isTextFieldDisabled = false
    var successCompletionHandler: (()->())?
    
    @Published var showResendText = false

}

Not very reusable but it works.... If you want to change the length don't forget to update the viewModel's otpField's didSet and the views textFieldOriginalWidth.

The idea here is to hide the TextField and make it seem like the user is typing in the boxes.

An Idea could be to shrink the TextField when user is typing by using the isEditing closure from the TextField. You would want to shrink it so the user can't paste text or get that "popup" or the textfield cursor.

Alhomaidhi
  • 339
  • 1
  • 7
  • Hi @Alhomaidhi, Thank you for your answer. Can I ask a favor if you can covert the broders into broken lines, just like in my screenshot? I tried it on my own but the line bounces down every time I input a number on a line. – Dreiohc Sep 02 '21 at 10:26
  • No problem :).. I did that above. It is your desired look but I think there should be an easier way to get the line instead of a rectangle with a a height if 0.5. – Alhomaidhi Sep 02 '21 at 10:34
  • Happy to help :) – Alhomaidhi Sep 02 '21 at 10:49
  • Sorry I'm trying to play with your code but still can't find how can I change the color of the line. For example. I typed in a number in the first digit, the 2nd line should turn into a different color like green. Example first digit the line is green, then I already input a number in first line, the next line should turn from black to green and so forth. when The text field is selected, the corresponding line should change to green. – Dreiohc Sep 02 '21 at 11:22
  • Here is the link to my post @Alhomaidhi if you interested again to answer https://stackoverflow.com/questions/69029915/change-color-of-lines-depending-on-the-focus-text-field – Dreiohc Sep 02 '21 at 12:04