In iOS 18, using TextKit to calculate the height of attributed strings is inaccurate. The same method produces correct results in systems below iOS 18.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(20, 40, 100, 0)];
textView.editable = NO;
textView.scrollEnabled = NO;
textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
textView.textContainer.lineFragmentPadding = 0;
textView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:textView];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"陈家坝好吃的撒海程邦达不差大撒把传达是吧才打卡吃吧金卡多措并举哈不好吃大杯茶十八次是吧"];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineSpacing = 4;
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, attributedString.length)];
[attributedString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:16] range:NSMakeRange(0, attributedString.length)];
[attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, attributedString.length)];
textView.attributedText = attributedString;
CGFloat height = [self test:attributedString];
textView.frame = CGRectMake(20, 40, 100, height);
}
- (CGFloat)test:(NSAttributedString *)attString {
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attString];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(100, CGFLOAT_MAX)];
textContainer.lineFragmentPadding = 0;
[layoutManager addTextContainer:textContainer];
[layoutManager ensureLayoutForTextContainer:textContainer];
CGFloat height = [layoutManager usedRectForTextContainer:textContainer].size.height;
return ceil(height);
}
Putting aside the different behavior between iOS 17 and iOS 18, I'd say that the code you provided isn't quite right, in that you render the text with TextKit2 but measure it with TextKit1.
Concretely, UITextView
by default uses TextKit2 in iOS 16 and later, and so textView
created in your viewDidLoad
is a TextKit2 text view. Unless you force textView
to fall back to TextKit1 with code not shown here (by accessing its layoutManager
property), it will use TextKit2 to render the text. And yet, your test:
method uses NSLayoutManager
, which is part of TextKit1, to measure the height of the text. It isn't a surprise that the height measured with TextKit1 is different from the height that TextKit2 needs to render the text.
You might consider using the same text engine, TextKit2 here, to measure and render the text. The result should be more matching. The following code example shows how to measure a piece of text with TextKit2:
func sizeOf(nsAttributedString: NSAttributedString, width: CGFloat) -> CGSize {
let textContainer = NSTextContainer(size: CGSize(width: width, height: 0))
textContainer.lineFragmentPadding = 0
let textLayoutManager = NSTextLayoutManager()
textLayoutManager.textContainer = textContainer
let textContentStorage = NSTextContentStorage()
textContentStorage.attributedString = nsAttributedString
textContentStorage.addTextLayoutManager(textLayoutManager)
textLayoutManager.ensureLayout(for: textLayoutManager.documentRange)
let rect = textLayoutManager.usageBoundsForTextContainer
return rect.size
}
Best,
——
Ziqiao Chen
Worldwide Developer Relations.