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

SwiftUI格式化TextField光标跳转至末尾问题求助

解决SwiftUI TextField动态格式化时光标跳至末尾的问题

问题说明

实现文本输入动态格式化(例如每两位字符添加一个空格)后,编辑过程中光标会自动跳至文本末尾,打断正常的编辑流程。

问题根源

SwiftUI原生TextField在绑定的文本值被外部修改时,会默认将光标重置到文本末尾。直接通过onChange修改绑定值会触发这一行为,且原生SwiftUI未提供直接控制光标位置的API。

解决方案

通过UIViewRepresentable桥接UIKit的UITextField,手动管理文本格式化与光标位置,确保编辑时光标停留在正确位置。

完整实现代码

import SwiftUI
import UIKit

struct ContentView: View {
    @State private var text = ""
    @State private var error: String?
    
    var body: some View {
        VStack {
            FormattedTextField(
                prompt: "Enter text here",
                text: $text,
                format: { input in
                    input
                        .lazy
                        .filter { $0 != " " }
                        .chunks(ofCount: 2)
                        .map { String($0) }
                        .joined(separator: " ")
                },
                parse: { formatted in
                    formatted.filter { $0 != " " }
                }
            )
            .padding(.horizontal)
            .textFieldStyle(.roundedBorder)
        }
        .padding()
    }
}

struct FormattedTextField: UIViewRepresentable {
    var prompt: String
    @Binding var text: String
    let format: (String) -> String
    let parse: (String) -> String
    
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.placeholder = prompt
        textField.borderStyle = .roundedRect
        textField.delegate = context.coordinator
        textField.addTarget(context.coordinator, action: #selector(Coordinator.textChanged), for: .editingChanged)
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        let formattedText = format(text)
        // 仅当文本不一致时更新,避免循环触发
        if uiView.text != formattedText {
            uiView.text = formattedText
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, UITextFieldDelegate {
        let parent: FormattedTextField
        
        init(parent: FormattedTextField) {
            self.parent = parent
        }
        
        @objc func textChanged(_ textField: UITextField) {
            guard let inputText = textField.text else { return }
            
            // 解析原始输入,移除格式符(空格)
            let parsedText = parent.parse(inputText)
            // 生成格式化后的文本
            let formattedText = parent.format(parsedText)
            
            // 计算光标应处的位置
            let cursorPosition = calculateCursorPosition(
                originalInput: inputText,
                formattedText: formattedText,
                selectedRange: textField.selectedRange
            )
            
            // 更新绑定的原始文本
            parent.text = parsedText
            // 设置格式化后的文本与光标位置
            textField.text = formattedText
            textField.selectedRange = cursorPosition
        }
        
        private func calculateCursorPosition(originalInput: String, formattedText: String, selectedRange: NSRange) -> NSRange {
            let originalCursorLoc = selectedRange.location
            // 统计原始输入中光标前的有效字符数(不含空格)
            let validCharsBeforeCursor = originalInput.prefix(originalCursorLoc)
                .filter { $0 != " " }
                .count
            
            // 在格式化后的文本中找到对应位置
            var formattedCursorLoc = 0
            var validCount = 0
            for char in formattedText {
                if validCount == validCharsBeforeCursor {
                    break
                }
                if char != " " {
                    validCount += 1
                }
                formattedCursorLoc += 1
            }
            
            // 处理光标在末尾的情况
            if validCharsBeforeCursor == parent.text.count {
                formattedCursorLoc = formattedText.count
            }
            
            return NSRange(location: formattedCursorLoc, length: 0)
        }
    }
}

extension Array {
    func chunks(ofCount chunkCount: Int) -> [[Element]] {
        var result: [[Element]] = []
        for index in stride(from: 0, to: count, by: chunkCount) {
            let lastIdx = Swift.min(count, index + chunkCount)
            result.append(Array(self[index ..< lastIdx]))
        }
        return result
    }
}

核心要点

  • UIKit桥接:借助UIViewRepresentable直接操作UITextField,获取并设置selectedRange来精准控制光标位置
  • 光标位置映射:根据原始输入中光标前的有效字符数,计算出格式化后文本对应的光标位置,确保编辑连贯性
  • 避免循环更新:在updateUIView中仅当文本内容不一致时才更新,防止重复触发文本变化逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 19:14:51