Codementor Events

SwiftUI custom OTP View, Firebase OTP authentication-Part 01

Published May 09, 2022
SwiftUI custom OTP View, Firebase OTP authentication-Part 01

This tutorial consists of three parts. In first part, we will create custom view that consists of countdown timer and line progress view that follows the countdown timer.

In part 2, we will create OTP-one time password view.

In part 3 we will show how to create reusable swift package that consists of OTP view from part 2 and implement firebase authentication, that will allow us to authenticate our user with phone number. Code will be available on GitHub.

So, shall we start?

First things first, lets create a new project in XCode. I will not go through process of creating project. As final solution, out project should look like this.

1_PE_JIz8J0qttLAl9E9fMFA.png

As we can see, we have bolierplate code, that we put in every solution like extensions, colors and some ViewModels. We will consider that code as self explanatory. Also, this code will be available on GitHub.

So lets start from simple functionalities like countdown view. Our CountDownView will look like this.

struct CountDownView : View {
    @ObservedObject var phoneViewModel: PhoneViewModel
    
    init(phoneViewModel: PhoneViewModel){
        self.phoneViewModel = phoneViewModel
    }
    
    var body: some View {
        Text(phoneViewModel.timeStr)
            .font(Font.system(size: 15))
            .foregroundColor(Color.textColorGray)
            .fontWeight(.semibold)
            .onReceive(phoneViewModel.timer) { _ in
                phoneViewModel.countDownString()
            }
    }
}

As we can see we are using ViewModel to abstract functionalities that this view is providing. PhoneViewModel consists of couple functions that are important for our CuntDownView.

class PhoneViewModel: ObservableObject {
    //MARK: vars
    var nowDate = Date()
    @Published var pin: String = ""
    @Published var phoneNumber = "+387 61 555 666"
    @Published var countryCodeNumber = ""
    @Published var country = ""
    @Published var code = ""
    @Published var timerExpired = false
    @Published var timeStr = ""
    @Published var timeRemaining = Constants.COUNTDOWN_TIMER_LENGTH
    var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    //MARK: functions
    func startTimer() {
        timerExpired = false
        timeRemaining = Constants.COUNTDOWN_TIMER_LENGTH
        self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    }
    
    func stopTimer() {
        timerExpired = true
        self.timer.upstream.connect().cancel()
    }

    func countDownString() {
        guard (timeRemaining > 0) else {
            self.timer.upstream.connect().cancel()
            timerExpired = true
            timeStr = String(format: "%02d:%02d", 00,  00)
            return
        }
        
        timeRemaining -= 1
        timeStr = String(format: "%02d:%02d", 00, timeRemaining)
    }
    
    func getPin(at index: Int) -> String {
        guard self.pin.count > index else {
            return ""
        }
        return self.pin[index]
    }
    
    func limitText(_ upper: Int) {
        if pin.count > upper {
            pin = String(pin.prefix(upper))
        }
    }
}

First things first, let check our timer variable.

var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

If we check definition, we can find out next explanation:

  • Timer.publish- Returns a publisher that repeatedly emits the current date on the given interval.
  • interval- The time interval on which to publish events. For example, a value of 0.5 publishes an event approximately every half-second. (in our case it is every 1 second)
  • runLoop- The run loop on which the timer runs. (It means that main Thread should auto initialize RunLoop)
  • mode- The run loop mode in which to run the timer.
  • autoconnect -Returns: A publisher which automatically connects to its upstream connectable publisher.

If we check our constants class ,we have declared our countdown timer length.

public static let COUNTDOWN_TIMER_LENGTH = 60

That means that we are counting down 60 seconds. Function that is important here is func countDownString(). We are checking if remaining time is larger than zero, and if it is we are returning current value of timeStr var, in mm:ss format. Otherwise we set our timer to expired and cancel further publishing of events from our publisher with self.timer.upstream.connect().cancel().

Whole purpose of countDownString func is that we are waiting for 60 seconds for out OTP code to arrive.

So in our ContDownView we create textfield that triggers action when our timer publisher emit an event. And that action is calling countDownString() func that display remaining time.

Next thing is to connect our progress view to countdown timer. For that purpose we have created LinearProgressBarView.

struct LinearProgressBarView: View {
    @State private var offset: CGFloat = CGFloat(Constants.COUNTDOWN_TIMER_LENGTH)
    @ObservedObject var phoneViewModel: PhoneViewModel
    
    init(phoneViewModel: PhoneViewModel){
        self.phoneViewModel = phoneViewModel
    }
    
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                Rectangle()
                    .frame(width: geometry.size.width , height: geometry.size.height)
                    .opacity(0.5)
                    .foregroundColor(Color.progressBarBackground)
                
                Rectangle()
                    .frame(width: (CGFloat(offset - CGFloat(phoneViewModel.timeRemaining)) * geometry.size.width)/offset, height: geometry.size.height)
                    .foregroundColor(Color.progressBarFill)
                    .animation(Animation.linear(duration: 1.0), value: offset)
            }.cornerRadius(45.0)
        }
        .frame(height: 5)
    }
}

As we can see, we are using PhoneViewModel and timeRemaining var to display progress of our progress view.

.frame(width: (CGFloat(offset — CGFloat(phoneViewModel.timeRemaining)) * geometry.size.width)/offset, height: geometry.size.height)

So when we start our app we should get this.

1_8hpEmU5JC7fQIfkzTHL_PA.gif

Now, only thing that we are left with is to implement OtpTextFieldView. Lets show what we want want achieve.

1_R8mcZMF7jinWapvWZAyvuA.gif

Every time we click on number we want to focus on next field, and when we click on return key we want to delete last number in otp textfield. We could have achieved this with mix of UIKit protocols and SwiftUI like UIViewRepresentable as explained here. So we manage our UIView inside of SwiftUI.

Instead we are going to use one TextField in ZStack with multiple Text Views for showing text from TextField.

public struct OtpTextFieldView: View {
    enum FocusField: Hashable {
        case field
    }
    @ObservedObject var phoneViewModel: PhoneViewModel
    @FocusState private var focusedField: FocusField?
    
    init(phoneViewModel: PhoneViewModel){
        self.phoneViewModel = phoneViewModel
    }
    
    private var backgroundTextField: some View {
        return TextField("", text: $phoneViewModel.pin)
            .frame(width: 0, height: 0, alignment: .center)
            .font(Font.system(size: 0))
            .accentColor(.blue)
            .foregroundColor(.blue)
            .multilineTextAlignment(.center)
            .keyboardType(.numberPad)
            .onReceive(Just(phoneViewModel.pin)) { _ in phoneViewModel.limitText(Constants.OTP_CODE_LENGTH) }
            .focused($focusedField, equals: .field)
            .task {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5)
                {
                    self.focusedField = .field
                }
            }
            .padding()
    }
    
    public var body: some View {
        ZStack(alignment: .center) {
            backgroundTextField
            HStack {
                ForEach(0..<Constants.OTP_CODE_LENGTH) { index in
                    ZStack {
                        Text(phoneViewModel.getPin(at: index))
                            .font(Font.system(size: 27))
                            .fontWeight(.semibold)
                            .foregroundColor(Color.textColorPrimary)
                        Rectangle()
                            .frame(height: 2)
                            .foregroundColor(Color.textColorPrimary)
                            .padding(.trailing, 5)
                            .padding(.leading, 5)
                            .opacity(phoneViewModel.pin.count <= index ? 1 : 0)
                    }
                }
            }
        }
    }

So, first we will use number pad as keyboard. Next, we need to make sure that our otp code length is less or equal than defined in our Constants.

public static let OTP_CODE_LENGTH = 6

For that we are using limitText func in PhoneViewModel.

For us to be able to populate corresponding TextView with correct pin at correct index, we are using func getPin(at index: Int) from PhoneViewModel.

And last, to show a keyboard at opening of the screen, we first set binding to focusField and set value of .field from our enum. Next we create task that is setting focusField bind variable to .field. This opens our keyboard when we show this view.

That is it, hope you have enjoyed this tutorial, and stay tuned for part 02 and more content.

The code is available in the GitHub repository:
GitHubff

Discover and read more posts from Kenan Begić
get started