You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Xcode12.3下SwiftUI实现OTP输入框下一位虚线随输入变色

OTP输入框动态高亮实现方案

不需要使用FocusState,基于你现有的隐藏TextField统一接收输入的实现,直接通过已输入的验证码长度判断下一位高亮位置即可,完全适配Xcode 12.3版本。

核心改动点

  • 给单个OTP位视图增加序号参数,传入当前已输入的验证码长度,动态计算底部虚线的颜色
  • 新增TextField第一响应者管理逻辑,适配点击唤起键盘的交互
  • 完全保留原有验证码输入限制、自动拆分等核心逻辑不需要修改

完整可运行代码

import SwiftUI

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: (()->())?
}

// 用于控制TextField第一响应者的辅助视图
struct TextFieldResponder: UIViewRepresentable {
    @Binding var isFirstResponder: Bool
    
    func makeUIView(context: Context) -> UIView {
        let view = UIView(frame: .zero)
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        DispatchQueue.main.async {
            guard let textField = uiView.superview?.superview as? UITextField else { return }
            if isFirstResponder && !textField.isFirstResponder {
                textField.becomeFirstResponder()
            } else if !isFirstResponder && textField.isFirstResponder {
                textField.resignFirstResponder()
            }
        }
    }
}

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, index: 1, currentCount: viewModel.otpField.count)
                    otpText(text: viewModel.otp2, index: 2, currentCount: viewModel.otpField.count)
                    otpText(text: viewModel.otp3, index: 3, currentCount: viewModel.otpField.count)
                    otpText(text: viewModel.otp4, index: 4, currentCount: viewModel.otpField.count)
                    otpText(text: viewModel.otp5, index: 5, currentCount: viewModel.otpField.count)
                    otpText(text: viewModel.otp6, index: 6, currentCount: viewModel.otpField.count)
                    
                }
                
                TextField("", text: $viewModel.otpField, onEditingChanged: { editing in
                    isFocused = editing
                })
                .frame(width: isFocused ? 0 : textFieldOriginalWidth, height: textBoxHeight)
                .disabled(viewModel.isTextFieldDisabled)
                .textContentType(.oneTimeCode)
                .foregroundColor(.clear)
                .accentColor(.clear)
                .background(Color.clear)
                .keyboardType(.numberPad)
                .background(TextFieldResponder(isFirstResponder: $isFocused))
            }
            .onTapGesture {
                isFocused = true
            }
        }
    }
    
    private func otpText(text: String, index: Int, currentCount: Int) -> some View {
        
        return Text(text)
            .font(.title)
            .frame(width: textBoxWidth, height: textBoxHeight)
            .background(VStack{
                Spacer()
                RoundedRectangle(cornerRadius: 1)
                    .frame(height: 1)
                    // 仅下一位的虚线显示为绿色,其他位为灰色
                    .foregroundColor(index == currentCount + 1 ? .green : .gray)
            })
            .padding(paddingOfBox)
    }
}

效果说明

  • 初始状态下还未输入任何数字时,第2位虚线自动显示为绿色
  • 输入第1位数字后,第3位虚线变为绿色,依次类推
  • 输入满6位数字后无下一位,所有虚线保持灰色
  • 点击OTP输入区域自动唤起键盘,符合普通输入框的交互习惯

内容的提问来源于stack exchange,提问作者Dreiohc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 04:42:02