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

iOS 16.1中UITextView的word/sentence分词器功能异常求助

iOS 16.1 UITextView 分词器异常问题及替代方案探讨

iOS 16.1版本中,UITextView内置的rangeEnclosingPosition(_:with:inDirection:)分词器函数出现异常:

  • word粒度调用始终返回nil,无法获取单词范围
  • sentence粒度仅对文本框最后一段句子生效,其他位置点击无法返回对应句子范围

原本基于UITextView的tokenizer(UITextInputTokenizer)属性实现的「点击选中整句」功能彻底失效,想问问有没有其他开发者碰到过这个问题?

最小复现代码

import UIKit

class ViewController: UIViewController {
    let textView = UITextView()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.isScrollEnabled = false
        textView.isEditable = false
        textView.font = .preferredFont(forTextStyle: .headline)
        
        textView.text = "Lorem ipsum dolor sit amet consectetur adipisicing elit. Odit, asperiores veniam praesentium repellat doloribus ut und. Soluta et hic velit aliquid totam aperiam ipsam ex odio, voluptatem iste saepe sit."
        
        self.view.addSubview(textView)
        
        NSLayoutConstraint.activate([
            textView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16),
            textView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16),
            textView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),
        ])
        
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tappedLabel(sender:)))
        textView.addGestureRecognizer(tapGestureRecognizer)
    }
    
    @objc func tappedLabel(sender: UITapGestureRecognizer) {
        guard sender.state == .ended else { return }
        
        let location = sender.location(in: textView)
        let textposition = textView.closestPosition(to: location)!
        
        /// iOS < 16.1 可正常获取点击位置对应的句子范围
        /// iOS 16.1 中仅最后一段句子能返回有效范围,其余情况返回nil
        let expandedRange = textView.tokenizer.rangeEnclosingPosition(textposition, with: .sentence, inDirection: .layout(.right))
        
        textView.becomeFirstResponder()
        textView.selectedTextRange = expandedRange
    }
}

替代方案:手动实现句子范围识别

既然系统API失效,我们可以通过文本内容结合点击位置对应的字符索引,手动定位句子的起始和结束位置。示例代码如下:

@objc func tappedLabel(sender: UITapGestureRecognizer) {
    guard sender.state == .ended else { return }
    
    let location = sender.location(in: textView)
    guard let textPosition = textView.closestPosition(to: location),
          let range = textView.textRange(from: textView.beginningOfDocument, to: textPosition),
          let text = textView.text,
          let selectedRange = textView.range(from: range) else { return }
    
    var startIndex = selectedRange.lowerBound
    var endIndex = selectedRange.upperBound
    
    // 向前遍历,定位句子起始边界(以句号、问号、感叹号+空格作为分隔)
    while startIndex > text.startIndex {
        let prevChar = text[text.index(before: startIndex)]
        if [".", "?", "!"].contains(String(prevChar)) {
            let nextIndex = text.index(after: text.index(before: startIndex))
            if nextIndex < text.endIndex && text[nextIndex] == " " {
                startIndex = nextIndex
                break
            }
        }
        startIndex = text.index(before: startIndex)
    }
    
    // 向后遍历,定位句子结束边界
    while endIndex < text.endIndex {
        let currentChar = text[endIndex]
        if [".", "?", "!"].contains(String(currentChar)) {
            endIndex = text.index(after: endIndex)
            // 若标点后有空格,将空格包含进范围
            if endIndex < text.endIndex && text[endIndex] == " " {
                endIndex = text.index(after: endIndex)
            }
            break
        }
        endIndex = text.index(after: endIndex)
    }
    
    // 将String.Index转换为UITextRange
    let startPosition = textView.position(from: textView.beginningOfDocument, offset: text.distance(from: text.startIndex, to: startIndex))!
    let endPosition = textView.position(from: textView.beginningOfDocument, offset: text.distance(from: text.startIndex, to: endIndex))!
    let expandedRange = textView.textRange(from: startPosition, to: endPosition)
    
    textView.becomeFirstResponder()
    textView.selectedTextRange = expandedRange
}

// UITextView扩展:将UITextRange转换为String.Index范围
extension UITextView {
    func range(from textRange: UITextRange) -> Range<String.Index>? {
        guard let start = textRange.start, let end = textRange.end else { return nil }
        let startOffset = offset(from: beginningOfDocument, to: start)
        let endOffset = offset(from: beginningOfDocument, to: end)
        guard let startIndex = text?.index(text.startIndex, offsetBy: startOffset),
              let endIndex = text?.index(text.startIndex, offsetBy: endOffset) else { return nil }
        return startIndex..<endIndex
    }
}

方案说明

  • 该方案通过手动判断标点符号边界实现句子范围定位,适配iOS 16.1及以上版本;
  • 可根据业务需求调整句子分隔规则(比如处理省略号、引号、换行等特殊场景);
  • 已有不少开发者在社区反馈过这个iOS 16.1的系统bug,目前Apple尚未发布修复补丁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 03:55:30