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

SwiftUI中单位数TextField输入与自动焦点跳转问题

解决SwiftUI中时间输入框自动焦点跳转问题

问题描述

我需要创建一个hh:mm格式的时间输入表单,要求严格校验输入有效性(比如24小时制下小时不能超过23),且不想用Picker(选60分钟太繁琐)。于是我拆了四个TextField分别对应时十位、时个位、分十位、分个位,添加了数字校验逻辑,希望输入有效数字后自动跳转到下一个输入框。

我用了一个遵循Hashable的枚举标记每个输入框,校验功能正常,但输入有效数字后,焦点虽然离开当前框,下一个框却没获得键盘焦点;同时在MacBook Pro模拟器运行时,调试器报错:
-[RTIInputSystemClient remoteTextInputSessionWithID:performInputOperation:] perform input operation requires a valid sessionID

我是Swift新手,官方文档太简洁看不懂,想知道这类功能的正确实现方式,希望得到示例代码和建议。

原始代码:

//
//  ContentView.swift
//  StackOverflowExample
//
//  Created by Mike Olson on 10/8/23.
//

import SwiftUI
import Combine

struct ContentView: View {
    
    // buch of hard-coded infra to automatically advance focus through the form
    enum Field: Hashable {
        case f_ch1
        case f_ch2
        case f_cm1
        case f_cm2
    }
    @FocusState private var focusedField: Field?

    // Clock digits
    @State private var ch1 = ""
    @State private var ch2 = ""
    @State private var cm1 = ""
    @State private var cm2 = ""

    var body: some View {
        VStack {
            HStack {
                TextField("h", text: $ch1)
                    .focusable(true, interactions: .edit)
                    .focused($focusedField, equals: .f_ch1)
                    .font(.title3)
                    .keyboardType(.numberPad)
                    .onReceive(Just(ch1)) { newValue in
                        let filtered = newValue.filter { "012".contains($0) } // first digit can be 0, 1, 2
                        if filtered != newValue {
                            self.ch1 = filtered
                        } else {
                            focusedField = .f_ch2
                        }
                    }
                TextField("h", text: $ch2)
                    .focusable(true, interactions: .edit)
                    .focused($focusedField, equals: .f_ch2)
                    .font(.title3)
                    .keyboardType(.numberPad)
                    .onReceive(Just(ch2)) { newValue in
                        let filtered = newValue.filter { (self.ch1 == "2" ? "0123" : "0123456789").contains($0) } // if the first digit's a 2, the second can't be bigger than 3
                        if filtered != newValue {
                            self.ch2 = filtered
                        } else {
                            focusedField = .f_cm1
                        }
                    }
                Text(":")
                    .font(.title3)
                TextField("m", text: $cm1)
                    .focusable(true, interactions: .edit)
                    .focused($focusedField, equals: .f_cm1)
                    .font(.title3)
                    .keyboardType(.numberPad)
                    .onReceive(Just(cm1)) { newValue in
                        let filtered = newValue.filter { "012345".contains($0) } // first minute digit is 0-5
                        if filtered != newValue {
                            self.cm1 = filtered
                        }
                    }
                TextField("m", text: $cm2)
                    .focusable(true, interactions: .edit)
                    .focused($focusedField, equals: .f_cm2)
                    .font(.title3)
                    .keyboardType(.numberPad)
                    .onReceive(Just(cm2)) { newValue in
                        let filtered = newValue.filter { "0123456789".contains($0) }
                        if filtered != newValue {
                            self.cm2 = filtered
                        }
                    }
            }
            .frame(maxWidth: 100)
        }
        .frame(width: 200, height: 200)
    }
}

#Preview {
    ContentView()
}

问题原因与修复方案

核心问题

之前的代码在文本变化时就触发焦点跳转,包括初始空值、手动修正输入的场景,导致焦点切换过于频繁,系统来不及正确响应焦点请求,同时也会触发模拟器的输入会话错误。

改进后的代码

import SwiftUI
import Combine

struct ContentView: View {
    enum Field: Hashable {
        case hourTens, hourUnits, minuteTens, minuteUnits
    }
    @FocusState private var focusedField: Field?

    @State private var hourTens = ""
    @State private var hourUnits = ""
    @State private var minuteTens = ""
    @State private var minuteUnits = ""

    // 辅助计算完整时间,用于后续验证或提交
    private var fullTime: String {
        "\(hourTens)\(hourUnits):\(minuteTens)\(minuteUnits)"
    }

    var body: some View {
        VStack(spacing: 20) {
            HStack(spacing: 8) {
                // 时十位
                TextField("H", text: $hourTens)
                    .focused($focusedField, equals: .hourTens)
                    .font(.title3)
                    .keyboardType(.numberPad)
                    .frame(width: 30, alignment: .center)
                    .onReceive(Just(hourTens)) { newValue in
                        // 只保留0-2的数字,且限制长度为1
                        let filtered = newValue.filter { "012".contains($0) }.prefix(1)
                        let newText = String(filtered)
                        if newText != newValue {
                            hourTens = newText
                        } else if newText.count == 1 {
                            // 输入有效且长度为1时,跳转到下一个输入框
                            focusedField = .hourUnits
                        }
                    }

                // 时个位
                TextField("H", text: $hourUnits)
                    .focused($focusedField, equals: .hourUnits)
                    .font(.title3)
                    .keyboardType(.numberPad)
                    .frame(width: 30, alignment: .center)
                    .onReceive(Just(hourUnits)) { newValue in
                        // 根据时十位决定允许的数字:如果是2,只能0-3,否则0-9
                        let allowedChars = hourTens == "2" ? "0123" : "0123456789"
                        let filtered = newValue.filter { allowedChars.contains($0) }.prefix(1)
                        let newText = String(filtered)
                        if newText != newValue {
                            hourUnits = newText
                        } else if newText.count == 1 {
                            focusedField = .minuteTens
                        }
                    }

                Text(":")
                    .font(.title3)

                // 分十位
                TextField("M", text: $minuteTens)
                    .focused($focusedField, equals: .minuteTens)
                    .font(.title3)
                    .keyboardType(.numberPad)
                    .frame(width: 30, alignment: .center)
                    .onReceive(Just(minuteTens)) { newValue in
                        let filtered = newValue.filter { "012345".contains($0) }.prefix(1)
                        let newText = String(filtered)
                        if newText != newValue {
                            minuteTens = newText
                        } else if newText.count == 1 {
                            focusedField = .minuteUnits
                        }
                    }

                // 分个位
                TextField("M", text: $minuteUnits)
                    .focused($focusedField, equals: .minuteUnits)
                    .font(.title3)
                    .keyboardType(.numberPad)
                    .frame(width: 30, alignment: .center)
                    .onReceive(Just(minuteUnits)) { newValue in
                        let filtered = newValue.filter { "0123456789".contains($0) }.prefix(1)
                        let newText = String(filtered)
                        if newText != newValue {
                            minuteUnits = newText
                        }
                    }
            }

            // 测试用:显示完整时间
            Text("输入的时间:\(fullTime)")
                .font(.caption)
        }
        .frame(width: 200, height: 200)
        .onAppear {
            // 初始焦点设置为时十位
            focusedField = .hourTens
        }
    }
}

#Preview {
    ContentView()
}

关键改进点

  1. 限制输入长度:用.prefix(1)确保每个TextField最多只能输入1个字符,避免多次触发跳转逻辑
  2. 调整跳转时机:只有当输入有效且文本长度为1时,才设置下一个焦点,避免空值或无效输入时的错误跳转
  3. 优化语义命名:把枚举和状态变量改成更易读的名称,提升代码可维护性
  4. 初始焦点设置:在.onAppear中设置初始焦点,用户打开页面即可直接输入
  5. 移除冗余设置:去掉.focusable(true, interactions: .edit),TextField默认支持编辑聚焦

效果说明

修复后,输入有效数字会自动跳转到下一个输入框,模拟器的输入会话错误也会消失。同时代码结构更清晰,校验逻辑更严谨,符合24小时制的时间规则。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 01:40:31