DEV Community

Ifeanyichukwu Sampson Ebenezer
Ifeanyichukwu Sampson Ebenezer

Posted on

How To Create Otp TextField In SwiftUI

Introduction

Having just started building native IOS applications using SwiftUI, It is different from building mobile apps using React Native. I struggled in making some components since I have less experience building Native Applications.

One of them is OTP InputField.

So in this article, I will be teaching you how I create my OTP Form.

Mindset

Building an OTP InputField is very simple if you understand the rules to follow.

  • Each field will not accept more than one value

  • On value, the input should change focus to the next one

Let's get started; by now I assume you have created a new project in your Xcode.

First, I will love to create a modifier for my InputFields.

Create a new SwiftUi View file OtpModifier.swift

import SwiftUI
import Combine

struct OtpModifer: ViewModifier {

    @Binding var pin : String

    var textLimt = 1

    func limitText(_ upper : Int) {
        if pin.count > upper {
            self.pin = String(pin.prefix(upper))
        }
    }


    //MARK -> BODY
    func body(content: Content) -> some View {
        content
            .multilineTextAlignment(.center)
            .keyboardType(.numberPad)
            .onReceive(Just(pin)) {_ in limitText(textLimt)}
            .frame(width: 45, height: 45)
            .background(Color.white.cornerRadius(5))
            .background(
                RoundedRectangle(cornerRadius: 5)
                    .stroke(Color("blueColor"), lineWidth: 2)
            )
    }
}
Enter fullscreen mode Exit fullscreen mode

with this code the binding variable pin will be holding the value from our input field, and the variable textlimit will be a placeholder for how many characters or the length of the value we want our input to receive, in my case I want each input to hold just one value, so my textlimit variable is one.

 func limitText(_ upper : Int) {
        if pin.count > upper {
            self.pin = String(pin.prefix(upper))
        }
    }
Enter fullscreen mode Exit fullscreen mode

The function limitText above takes an argument of type Integer upper and checks if the value on the input is greater than the upper argument. If true, it will assign pin a new value that returns just the first character in the initial value provided.

Create another SwiftUi View file OtpFormFieldView.swift

import SwiftUI
import Combine

struct OtpFormFieldView: View {
    //MARK -> PROPERTIES

    enum FocusPin {
        case  pinOne, pinTwo, pinThree, pinFour
    }

    @FocusState private var pinFocusState : FocusPin?
    @State var pinOne: String = ""
    @State var pinTwo: String = ""
    @State var pinThree: String = ""
    @State var pinFour: String = ""


    //MARK -> BODY
    var body: some View {
            VStack {

                Text("Verify your Email Address")
                    .font(.title2)
                    .fontWeight(.semibold)


                Text("Enter 4 digit code we'll text you on Email")
                    .font(.caption)
                    .fontWeight(.thin)
                    .padding(.top)

                HStack(spacing:15, content: {

                    TextField("", text: $pinOne)
                        .modifier(OtpModifer(pin:$pinOne))
                        .onChange(of:pinOne){newVal in
                            if (newVal.count == 1) {
                                pinFocusState = .pinTwo
                            }
                        }
                        .focused($pinFocusState, equals: .pinOne)

                    TextField("", text:  $pinTwo)
                        .modifier(OtpModifer(pin:$pinTwo))
                        .onChange(of:pinTwo){newVal in
                            if (newVal.count == 1) {
                                pinFocusState = .pinThree
                            }
                        }
                        .focused($pinFocusState, equals: .pinTwo)


                    TextField("", text:$pinThree)
                        .modifier(OtpModifer(pin:$pinThree))
                        .onChange(of:pinThree){newVal in
                            if (newVal.count == 1) {
                                pinFocusState = .pinFour
                            }
                        }
                        .focused($pinFocusState, equals: .pinThree)


                    TextField("", text:$pinFour)
                        .modifier(OtpModifer(pin:$pinFour))
                        .focused($pinFocusState, equals: .pinFour)


                })
                .padding(.vertical)


                Button(action: {}, label: {
                    Spacer()
                    Text("Veify")
                        .font(.system(.title3, design: .rounded))
                        .fontWeight(.semibold)
                        .foregroundColor(.white)
                    Spacer()
                })
                .padding(15)
                .background(Color.blue)
                .clipShape(Capsule())
                .padding()
            }

    }
}

struct OtpFormFieldView_Previews: PreviewProvider {
    static var previews: some View {
        OtpFormFieldView()
    }
}
Enter fullscreen mode Exit fullscreen mode

Now in this file, we combined what we have been building with a couple of new things. These new things include FocusState FocusState is helping us keep an eye on the inputField the user is currently typing on and notifies us when the input is up to the textlimit

We have specified in our case when the input receives a value that is equal to one, it will notify our FocusState and then the focus will be changed to the next input till we get to the last inputField

import SwiftUI
import Combine

struct OtpFormFieldView: View {
    //MARK -> PROPERTIES

    enum FocusPin {
        case  pinOne, pinTwo, pinThree, pinFour
    }

    @FocusState private var pinFocusState : FocusPin?
    @State var pinOne: String = ""
    @State var pinTwo: String = ""
    @State var pinThree: String = ""
    @State var pinFour: String = ""


    //MARK -> BODY
    var body: some View {
            VStack {

                Text("Verify your Email Address")
                    .font(.title2)
                    .fontWeight(.semibold)


                Text("Enter 4 digit code we'll text you on Email")
                    .font(.caption)
                    .fontWeight(.thin)
                    .padding(.top)

                HStack(spacing:15, content: {

                    TextField("", text: $pinOne)
                        .modifier(OtpModifer(pin:$pinOne))
                        .onChange(of:pinOne){newVal in
                            if (newVal.count == 1) {
                                pinFocusState = .pinTwo
                            }
                        }
                        .focused($pinFocusState, equals: .pinOne)

                    TextField("", text:  $pinTwo)
                        .modifier(OtpModifer(pin:$pinTwo))
                        .onChange(of:pinTwo){newVal in
                            if (newVal.count == 1) {
                                pinFocusState = .pinThree
                            }else {
                                if (newVal.count == 0) {
                                    pinFocusState = .pinOne
                                }
                            }
                        }
                        .focused($pinFocusState, equals: .pinTwo)


                    TextField("", text:$pinThree)
                        .modifier(OtpModifer(pin:$pinThree))
                        .onChange(of:pinThree){newVal in
                            if (newVal.count == 1) {
                                pinFocusState = .pinFour
                            }else {
                                if (newVal.count == 0) {
                                    pinFocusState = .pinTwo
                                }
                            }
                        }
                        .focused($pinFocusState, equals: .pinThree)


                    TextField("", text:$pinFour)
                        .modifier(OtpModifer(pin:$pinFour))
                        .onChange(of:pinFour){newVal in
                            if (newVal.count == 0) {
                                pinFocusState = .pinThree
                            }
                        }
                        .focused($pinFocusState, equals: .pinFour)


                })
                .padding(.vertical)


                Button(action: {}, label: {
                    Spacer()
                    Text("Veify")
                        .font(.system(.title3, design: .rounded))
                        .fontWeight(.semibold)
                        .foregroundColor(.white)
                    Spacer()
                })
                .padding(15)
                .background(Color.blue)
                .clipShape(Capsule())
                .padding()
            }

    }
}

struct OtpFormFieldView_Previews: PreviewProvider {
    static var previews: some View {
        OtpFormFieldView()
    }
}
Enter fullscreen mode Exit fullscreen mode

I updated the OtpFormFieldView.swift, allowing It to change focus to the previous InputField if the user deletes the input value. Now we have a functional OTP InputField to build on.

Preview

Image description

Link

You can get the code from this article from GitHub

Conclusion

I know this will not be the most efficient way of achieving this, but this works for my use case, and you can update the code based on how many inputField you want for your use case

Top comments (2)

Collapse
 
ana_marquez_fe6478ad833de profile image
Ana Marquez

Great post! One question tho, what about handling the one time passcode that you receive from a message? Have you find a way to handle that? I was able to do it with UIKit, but still not sure how to do with SwiftUI

Collapse
 
magistraapta profile image
magistra apta

this is what I'm searching for. thanks