Requirement
Extract text that fits a specified container size from a long string, ensuring consistent display across different system versions.
Issue
When using layoutManager.enumerateLineFragments from TextKit for line-by-line text segmentation, discrepancies arise at the end of lines when full-width characters or phrases appear. For instance, while TextKit deems a line as fully displayable without breaking, the UI component in iOS 17 forcibly wraps the phrase onto the next line. This issue does not occur in iOS 16 where line ebreaking and segmentation align perfectly.
import SwiftUI
struct SplitTextDemo: View {
let fontSize: CGFloat = 14
let containerSize: CGSize = CGSize(width: 300, height: 100)
@State var contentString: String = "18、在一个提示中多次重复某个特定的词短语。nn测试文案是否会发生偏移,查看具体位置和文末。iOS16则不会有这个问题,换行和分割是完全一样,但在iOS17中则会出现显示和textkit裁切不一致的情况。"
@State var splitString: String = ""
var body: some View {
VStack {
Text("Original text:")
TextField("", text: $contentString, axis: .vertical)
.font(.system(size: fontSize))
.frame(width: containerSize.width)
.background {
Color.blue
}
Text("Extracted text:")
TextField("", text: $splitString, axis: .vertical)
.font(.system(size: fontSize))
.frame(width: containerSize.width, height: containerSize.height)
.background {
Color.orange
}
}
.onChange(of: contentString, perform: { value in
splitString = splitTextIntoLines(text: contentString, fontSize: fontSize)
})
}
func splitTextIntoLines(text: String, fontSize: CGFloat) -> String {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakStrategy = .pushOut
// Create NSTextStorage and specify the font size
let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: fontSize), .paragraphStyle: paragraphStyle]
let textStorage = NSTextStorage(string: text, attributes: attributes)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
// Create an NSTextContainer of specified size
let textContainer = NSTextContainer(size: containerSize)
textContainer.maximumNumberOfLines = 0 // Indicates no maximum number of lines
textContainer.lineBreakMode = .byCharWrapping // Set line breaking mode to character wrapping
textContainer.lineFragmentPadding = 0.1
layoutManager.addTextContainer(textContainer)
layoutManager.ensureLayout(for: textContainer)
// Return the split text lines
var lines: [String] = [] // Store each split text line
// Traverse the text within the display range
layoutManager.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: layoutManager.numberOfGlyphs)) { _, usedRect, _, glyphRange, stop in
if !usedRect.isEmpty {
// Get the single line text
let characterRange = Range(glyphRange, in: text)
if let characterRange = characterRange {
// Retrieve line text from original text and add to lines array
let line = String(text[characterRange])
lines.append(line)
print("Split line: (line)")
}
} else {
stop.pointee = true
}
}
return lines.joined(separator: "")
}
}
#Preview {
SplitTextDemo()
}
-
I adjusted the lineFragmentPadding in NSLayoutManager to match the padding of the UI components, which had no impact on the line breaking issue.
-
I switched NSLayoutManager.lineBreakMode to byCharWrapping in an attempt to force character-by-character wrapping, but it did not affect the issue.
-
I set paragraphStyle.lineBreakStrategy to .pushOut. This resolved the issue when phrases ended the sentence without full-width characters. However, the display still did not match TextKit’s segmentation when full-width characters were present.