OTP Verification Template

OTP Verification Template

by dockui

A modern SwiftUI OTP verification screen with animated input fields, auto-focus handling, resend countdown timer, & error feedback with shake animation.

Platform
SwiftUI
Code
Native Swift
Format
Screen
Access
Free

Code Snippet

import SwiftUI

struct OTPVerificationTemplate: View {
    @State private var otpFields: [String] = Array(repeating: "", count: 6)
    @State private var currentField: Int = 0
    @State private var isVerifying: Bool = false
    @State private var showSuccess: Bool = false
    @State private var showError: Bool = false
    @FocusState private var fieldFocus: Int?
    
    // Timer for resend countdown
    @State private var timeRemaining: Int = 30
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 32) {
                    // Header
                    VStack(spacing: 12) {
                        Image(systemName: "lock.shield.fill")
                            .font(.system(size: 48))
                            .foregroundStyle(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))
                            .symbolEffect(.bounce, options: .repeating)
                        
                        Text("Verification Code")
                            .font(.title2)
                            .fontWeight(.bold)
                        
                        Text("We've sent a verification code to\n+1 (123) 456-7890")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                            .multilineTextAlignment(.center)
                    }
                    .padding(.top, 20)
                    
                    // OTP Input Fields
                    HStack(spacing: 12) {
                        ForEach(0..<6) { index in
                            OTPTextField(
                                text: $otpFields[index],
                                isFocused: fieldFocus == index,
                                isError: showError
                            )
                            .focused($fieldFocus, equals: index)
                            .onChange(of: otpFields[index]) { oldValue, newValue in
                                handleInput(at: index)
                            }
                        }
                    }
                    .padding(.horizontal)
                    
                    // Verification Button
                    Button(action: verifyCode) {
                        ZStack {
                            if isVerifying {
                                ProgressView()
                                    .tint(.white)
                            } else {
                                Text("Verify")
                                    .fontWeight(.semibold)
                            }
                        }
                        .frame(maxWidth: .infinity)
                        .frame(height: 50)
                        .foregroundColor(.white)
                        .background(isInputComplete ? Color.blue : Color.blue.opacity(0.5))
                        .cornerRadius(12)
                    }
                    .disabled(!isInputComplete || isVerifying)
                    .padding(.horizontal)
                    
                    // Resend Code
                    VStack(spacing: 8) {
                        if timeRemaining > 0 {
                            Text("Resend code in \(timeRemaining)s")
                                .font(.footnote)
                                .foregroundColor(.secondary)
                        } else {
                            Button("Resend Code") {
                                resendCode()
                            }
                            .font(.footnote)
                            .foregroundColor(.blue)
                        }
                    }
                }
                .padding()
            }
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button(action: {}) {
                        Image(systemName: "chevron.left")
                            .fontWeight(.semibold)
                    }
                }
            }
            .onChange(of: currentField) { oldValue, newValue in
                fieldFocus = newValue
            }
            .onReceive(timer) { _ in
                if timeRemaining > 0 {
                    timeRemaining -= 1
                }
            }
            .alert("Verification Successful", isPresented: $showSuccess) {
                Button("Continue") {}
            }
        }
    }
    
    private var isInputComplete: Bool {
        otpFields.allSatisfy { !$0.isEmpty }
    }
    
    private func handleInput(at index: Int) {
        let value = otpFields[index]
        
        // Handle backspace
        if value.isEmpty && index > 0 {
            currentField = index - 1
            return
        }
        
        // Limit to single digit and move to next field
        if value.count > 1 {
            otpFields[index] = String(value.suffix(1))
        }
        
        if !value.isEmpty && index < otpFields.count - 1 {
            currentField = index + 1
        }
        
        // Reset error state when user starts typing
        if showError {
            showError = false
        }
    }
    
    private func verifyCode() {
        isVerifying = true
        showError = false
        
        // Simulate verification
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            isVerifying = false
            let code = otpFields.joined()
            
            // Demo: Show success for "123456", error for others
            if code == "123456" {
                withAnimation {
                    showSuccess = true
                }
            } else {
                withAnimation(.easeInOut) {
                    showError = true
                }
            }
        }
    }
    
    private func resendCode() {
        // Simulate resend
        timeRemaining = 30
        
        // Haptic feedback
        let generator = UINotificationFeedbackGenerator()
        generator.notificationOccurred(.success)
    }
}

// MARK: - OTP Text Field
struct OTPTextField: View {
    @Binding var text: String
    var isFocused: Bool
    var isError: Bool
    
    var body: some View {
        TextField("", text: $text)
            .keyboardType(.numberPad)
            .textContentType(.oneTimeCode)
            .multilineTextAlignment(.center)
            .frame(width: 45, height: 55)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(
                        isFocused ? Color.blue :
                            isError ? Color.red :
                            Color.gray.opacity(0.3),
                        lineWidth: isFocused || isError ? 2 : 1
                    )
            )
            .shakeEffect(trigger: isError)
    }
}

// MARK: - Shake Effect Modifier
struct ShakeEffect: AnimatableModifier {
    var trigger: Bool
    var animatableData: CGFloat = 0
    
    func body(content: Content) -> some View {
        content
            .offset(x: trigger ? sin(animatableData * .pi * 2) * 5 : 0)
            .animation(trigger ? .easeInOut(duration: 0.5) : .default, value: trigger)
    }
}

extension View {
    func shakeEffect(trigger: Bool) -> some View {
        modifier(ShakeEffect(trigger: trigger))
    }
}

#Preview {
    OTPVerificationTemplate()
} 

Similar Blogs

View All Articles
How to Translate Xcode String Catalogs with AI

How to Translate Xcode String Catalogs with AI

Learn how to efficiently translate Xcode string catalogs using AI fast, streamlining localization for your iOS and SwiftUI applications.

by dockui
May 4
How to optimize your SwiftUI app in 2026

How to optimize your SwiftUI app in 2026

Discover top strategies to optimize your SwiftUI app in 2026, enhancing performance, responsiveness, and maintainability with modern concurrency.

by dockui
Apr 29
SwiftUI: iOS Confirmation Alert (Confirm + Cancel)

SwiftUI: iOS Confirmation Alert (Confirm + Cancel)

SwiftUI iOS confirmation alert with Confirm and Cancel buttons. Beginners guide to building safe actions, destructive deletes, and commit dialogs.

by dockui
Nov 11
SwiftUI: IOS Haptic Feedback Guide 2026

SwiftUI: IOS Haptic Feedback Guide 2026

Add click to vibrate haptic feedback in SwiftUI. Full guide to UIImpactFeedbackgenerator, success, error, selection, and tap vibration patterns with code.

by dockui
Nov 11

Code copied to clipboard!