SWIFT:停止语音朗读截断的字符串

3
在一个标签中,我赋值了一个很长的字符串,并设置其 .lineBreakMode = .buTruncatingTail,但是当我这样做并尝试在其上使用 VoiceOver 时,它最终会读取整个字符串,而不仅仅是屏幕上的内容。以下是一个示例:
string.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
srring.lineBreakMode = .buTrucatingTail

以下是翻译的结果:

屏幕上显示的内容如下:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco...

但语音播报会读取整个字符串。

有人知道怎么让它在三个点截断停止吗?或者如何将可访问性标签设置为屏幕上显示的内容(因为文本长度根据设备而异)?

提前感谢。


你尝试过使用 string.accessibilityHint = "Lorem ipsum dolor sit amet short" 吗? - Dmytro Rostopira
我不太理解这个短的东西。 - Iago Salomon
2
截断仅用于显示。VoiceOver将始终读出您字符串的整个文本。为了达到您的目标,您可以使用TextKit获取最后一个字符的索引,以创建可访问的文本以供朗读。 - XLE_22
1
你不应该尝试截断 accessibilityLabel 中的字符串。这适用于所有断言技术,而不仅仅是 VoiceOver。你在问题中提到截断会随着设备大小而改变,你应该将 VoiceOver 视为一种可以“显示”任意长度字符串的设备。 - Richard Stelling
@RichardStelling:按照这种思路,每次需要新的布局时,他都可以适应并重新定义accessibilityLabel...这似乎很有趣,不是吗?所有用户将拥有与屏幕上相同的信息。 - XLE_22
显示剩余2条评论
1个回答

1
[...] 当我这样做并尝试在其上使用VoiceOver时,它最终会读取整个字符串,而不仅仅是屏幕中的内容[...] VoiceOver会读取整个字符串。
正如我在评论中所说,截断仅用于显示
VoiceOver始终会朗读出字符串的整个文本,并且不关心屏幕上显示的内容。
这就像accessibilityLabel可能与显示的内容不同一样:在这里,accessibilityLabel是整个字符串内容。

有人知道如何使它停留在省略号处吗?

你的问题引起了我的好奇心,我决定研究一下这个问题。
我找到了一个使用TextKit的解决方案,其基础应该是已知的:如果不是这种情况 ⟹ Apple doc

⚠️ 主要思路是确定最后一个可见字符的索引,以便提取显示的子字符串并将其分配给accessibilityLabel。 ⚠️

初始假设:使用与问题中定义的lineBreakMode相同的UILabel (byTruncationTail)
我在playground中编写了以下所有代码,并使用Xcode空白项目确认了结果。

import Foundation
import UIKit

extension UILabel {
    var displayedLines: (number: Int?, lastIndex: Int?) {
    
        guard let text = text else {
            return (nil, nil)
        }
    
    let attributes: [NSAttributedString.Key: UIFont] = [.font:font]
    let attributedString = NSAttributedString(string: text,
                                              attributes: attributes)
    
    //Beginning of the TextKit basics...
    let textStorage = NSTextStorage(attributedString: attributedString)
    let layoutManager = NSLayoutManager()
    textStorage.addLayoutManager(layoutManager)
    
    let textContainerSize = CGSize(width: frame.size.width,
                                   height: CGFloat.greatestFiniteMagnitude)
    let textContainer = NSTextContainer(size: textContainerSize)
    textContainer.lineFragmentPadding = 0.0 //Crucial to get the most accurate results.
    textContainer.lineBreakMode = .byTruncatingTail
    
    layoutManager.addTextContainer(textContainer)
    //... --> end of the TextKit basics
    
    var glyphRangeMax = NSRange()
    let characterRange = NSMakeRange(0, attributedString.length)
    //The 'glyphRangeMax' variable will store the appropriate value thanks to the following method.
    layoutManager.glyphRange(forCharacterRange: characterRange,
                             actualCharacterRange: &glyphRangeMax)
    
    print("line : sentence -> last word")
    print("----------------------------")
    
    var truncationRange = NSRange()
    var fragmentNumber = ((self.numberOfLines == 0) ? 1 : 0)
    var globalHeight: CGFloat = 0.0
    
    //Each line fragment of the layout manager is enumerated until the truncation is found.
    layoutManager.enumerateLineFragments(forGlyphRange: glyphRangeMax) { rect, usedRect, textContainer, glyphRange, stop in
        
        globalHeight += rect.size.height
        
        if (self.numberOfLines == 0) {
            if (globalHeight > self.frame.size.height) {
                print("⚠️ Constraint ⟹ height of the label ⚠️")
                stop.pointee = true //Stops the enumeration and returns the results immediately.
            }
        } else {
            if (fragmentNumber == self.numberOfLines) {
                print("⚠️ Constraint ⟹ number of lines ⚠️")
                stop.pointee = true
            }
        }
        
        if (stop.pointee.boolValue == false) {
            fragmentNumber += 1
            truncationRange = NSRange()
            layoutManager.characterRange(forGlyphRange: NSMakeRange(glyphRange.location, glyphRange.length),
                                         actualGlyphRange: &truncationRange)
            
            let sentenceInFragment = self.sentenceIn(truncationRange)
            let line = (self.numberOfLines == 0) ? (fragmentNumber - 1) : fragmentNumber
            print("\(line) : \(sentenceInFragment) -> \(lastWordIn(sentenceInFragment))")
        }
    }
    
    let lines = ((self.numberOfLines == 0) ? (fragmentNumber - 1) : fragmentNumber)
    return (lines, (truncationRange.location + truncationRange.length - 2))
}

//Function to get the sentence of a line fragment
func sentenceIn(_ range: NSRange) -> String {
    
    var extractedString = String()
    
    if let text = self.text {
        
        let indexB = text.index(text.startIndex,
                                offsetBy:range.location)
        let indexF = text.index(text.startIndex,
                                offsetBy:range.location+range.length)
        
        extractedString = String(text[indexB..<indexF])
    }
    return extractedString
}
}


//Function to get the last word of the line fragment
func lastWordIn(_ sentence: String) -> String {

    var words = sentence.components(separatedBy: " ")
    words.removeAll(where: { $0 == "" })

    return (words.last == nil) ? "o_O_o" : (words.last)!
}

一旦完成,我们需要创建标签:
let labelFrame = CGRect(x: 20,
                        y: 50,
                        width: 150,
                        height: 100)
var testLabel = UILabel(frame: labelFrame)
testLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

...并测试代码:

testLabel.numberOfLines = 3

if let _ = testLabel.text {
    let range = NSMakeRange(0,testLabel.displayedLines.lastIndex! + 1)
    print("\nNew accessibility label to be implemented ⟹ \"\   (testLabel.sentenceIn(range))\"")
}

游乐场的调试区域显示3行结果: enter image description here ...如果想要“尽可能多的行”,我们会得到: enter image description here 这似乎很有效。 将此原理包含在您自己的代码中,您将能够使VoiceOver读取屏幕上显示的标签的截断内容。

2
谢谢,那是一份非常出色和详尽的答案,感谢您花费时间和精力编写它。 - Iago Salomon
在我的项目中,语音提示应该只说设备图像,而不是信用卡或心脏图。你有什么想法吗? - keshav
@keshav,您介意用详细的解释在新话题中提出您的问题吗?这样可以尽可能地提供最佳答案。 - XLE_22

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接