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 } }
核心逻辑说明
- 分离数据与视图:用
rawPhoneNumber存储纯数字原始数据,格式化文本通过计算属性生成,避免直接修改绑定文本导致TextField重置光标。 - 光标偏移修正:根据输入前后的文本差异(尤其是连字符的增减)调整光标位置,确保光标停留在用户预期的输入位置。
内容的提问来源于stack exchange,提问作者Samet Çağrı Aktepe
相关产品推荐
相关产品推荐

