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

SwiftUI TextField格式化时光标自动跳至末尾问题求助

SwiftUI TextField自定义格式化后光标跳转问题的纯SwiftUI解决方案

问题场景

给TextField做手机号格式化(要求格式XXX-XXX-XX-XX)时,用onChange修改文本后,光标会自动跳到文本末尾。比如删了索引5的字符,光标本该停在那继续输入,结果直接跳去末尾,输入体验很差。之前找到的解决方案都是UIKit的,现在要纯SwiftUI的实现。

纯SwiftUI解决办法

核心是在更新格式化文本的同时,计算并恢复正确的光标位置,这里提供两种实用方案:

方案一:利用TextField的selection绑定(完全纯SwiftUI)

这种方案完全基于SwiftUI原生API,通过selection参数跟踪光标位置,更新文本时同步调整光标偏移:

import SwiftUI

struct TextFieldTestView: View {
    // 存储纯数字的原始数据,与格式化文本分离
    @State private var rawPhoneNumber: String = ""
    // 跟踪光标选中范围(用于定位光标)
    @State private var cursorSelection: Range<String.Index>?
    
    // 实时计算格式化后的手机号
    var formattedPhone: String {
        formatPhoneNumber(rawPhoneNumber)
    }
    
    var body: some View {
        VStack {
            // 使用带selection参数的TextField初始化器
            TextField("输入手机号", text: Binding(
                get: { formattedPhone },
                set: { inputText in
                    // 提取输入中的纯数字,更新原始数据
                    let newDigits = inputText.filter { $0.isNumber }
                    let oldFormatted = formattedPhone
                    
                    rawPhoneNumber = newDigits
                    let newFormatted = formattedPhone
                    
                    // 计算光标在输入框内的偏移量
                    let cursorOffsetInInput = inputText.distance(from: inputText.startIndex, to: inputText.endIndex)
                    var adjustedOffset = cursorOffsetInInput
                    
                    // 对比新旧格式化文本的连字符数量差异,修正光标偏移
                    let oldHyphenCount = oldFormatted.filter { $0 == "-" }.count
                    let newHyphenCount = newFormatted.filter { $0 == "-" }.count
                    adjustedOffset += (newHyphenCount - oldHyphenCount)
                    
                    // 确保偏移量合法,设置光标位置
                    if adjustedOffset >= 0 && adjustedOffset <= newFormatted.count {
                        let start = newFormatted.startIndex
                        cursorSelection = start..<newFormatted.index(start, offsetBy: adjustedOffset)
                    }
                }
            ), selection: $cursorSelection)
            .textFieldStyle(.roundedBorder)
        }
        .padding()
    }
    
    // 原有格式化逻辑不变
    func formatPhoneNumber(_ number: String) -> String {
        let digits = number.filter { $0.isNumber }
        let maxLength = min(digits.count, 10)
        let chars = Array(digits.prefix(maxLength))
        var formatted = ""
        
        for (index, char) in chars.enumerated() {
            if index == 3 || index == 6 || index == 8 {
                formatted += "-"
            }
            formatted += String(char)
        }
        
        return formatted
    }
}

方案二:轻量结合UIKit(兼容旧版本SwiftUI)

如果你的SwiftUI版本对selection支持有限,可以用这个方法,仅用到少量UIKit API,整体仍为SwiftUI写法:

import SwiftUI

struct TextFieldTestView: View {
    @State private var rawPhoneNumber: String = ""
    @FocusState private var isFocused: Bool
    
    var formattedPhone: String {
        formatPhoneNumber(rawPhoneNumber)
    }
    
    var body: some View {
        VStack {
            TextField("输入手机号", text: Binding(
                get: { formattedPhone },
                set: { inputText in
                    let newDigits = inputText.filter { $0.isNumber }
                    let cursorOffset = inputText.distance(from: inputText.startIndex, to: inputText.endIndex)
                    
                    rawPhoneNumber = newDigits
                    
                    // 延迟更新光标,确保TextField已刷新文本
                    DispatchQueue.main.async {
                        // 定位当前活跃的TextField实例
                        guard let textField = UIApplication.shared.connectedScenes
                                .compactMap({ $0 as? UIWindowScene })
                                .first?.windows
                                .first?.rootViewController?.view
                                .subviews
                                .compactMap({ $0 as? UITextField })
                                .first else { return }
                        
                        // 设置光标到预期位置
                        if cursorOffset <= formattedPhone.count {
                            let targetPosition = textField.position(from: textField.beginningOfDocument, offset: cursorOffset)
                            textField.selectedTextRange = textField.textRange(from: targetPosition!, to: targetPosition!)
                        }
                    }
                }
            ))
            .textFieldStyle(.roundedBorder)
            .focused($isFocused)
        }
        .padding()
    }
    
    func formatPhoneNumber(_ number: String) -> String {
        let digits = number.filter { $0.isNumber }
        let maxLength = min(digits.count, 10)
        let chars = Array(digits.prefix(maxLength))
        var formatted = ""
        
        for (index, char) in chars.enumerated() {
            if index == 3 || index == 6 || index == 8 {
                formatted += "-"
            }
            formatted += String(char)
        }
        
        return formatted
    }
}

核心逻辑说明

  1. 分离数据与视图:用rawPhoneNumber存储纯数字原始数据,格式化文本通过计算属性生成,避免直接修改绑定文本导致TextField重置光标。
  2. 光标偏移修正:根据输入前后的文本差异(尤其是连字符的增减)调整光标位置,确保光标停留在用户预期的输入位置。

内容的提问来源于stack exchange,提问作者Samet Çağrı Aktepe

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 18:43:16