Creating Otp InputField in SwiftUI
Table of contents
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)
)
}
}
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))
}
}
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()
}
}
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 in our case 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 a new 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()
}
}
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
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