如何求UILabel最后一个字符的Frame

前因

是咁的。

最近重构项目代码,看到某个页面的页面元素不多,UI前人实现起来却用了一大pie代码(主要方法是sizeWithFont)。
经分析,这pie代码是用来求UILabel最后一个character的frame的,得到frame然后判断是否有足够宽度放置诸如HOT、顶等图片,是则放置,否则换行放置,并在点击时候做相应的动画。

乍一看,心生疑惑。UILabel应该有提供相关方法吧。各种头文件里转了一圈,结果。当然没有。
(我的天呐+捂嘴状
又转了一圈

Demo

I'm Demo

后果

...
倒是在NSLayoutManager.h里找到一个近似的。

- (CGRect)boundingRectForGlyphRange:(NSRange)glyphRange inTextContainer:(NSTextContainer *)container

用法也很简单,提供一个glyphRange以及对应的textContainer,便返回该字形在textContainer里的frame。
我抓了抓头,这是要用到TextKit。TextKit虽然是iOS7就有的产物,但其三件套NSTextStorage、NSLayoutManager、NSTextContainer有些同学(就是我)平时用得不是很多,这UILabel又没有自带三件套,需要自己实现。(似乎UITextView有?

好吧,顺便复习下加深下理解。

  1. Textkit的三件法宝:。objc有篇译文提到可以把文本系统看做一个MVC架构,即NSTextStorage -> Model,NSLayoutManager -> Controller, NSTextContainer -> View,我们可以这样理解,就不容易弄混。

  2. 就我们的需求来讲,配置好三件套的互相依赖,给NSTextStorage赋值,设置NSTextContainer的属性,NSLayoutManager就会帮我们布局。布局完成后,我们就可以向NSLayoutManager询问布局的情况,也可以通过一些代理方法改变其布局。

  3. 没有3了。

好的。代码时间。

- (void)setupBasic
{
    self.textStorage = [NSTextStorage new];
    self.layoutManager = [NSLayoutManager new];
    self.textContainer = [NSTextContainer new];
    [self.textStorage addLayoutManager:self.layoutManager];
    [self.layoutManager addTextContainer:self.textContainer];
}

上面这段代码是初始化三件套。

- (void)configWithLabel:(UILabel *)label
{
  self.textContainer.size = label.bounds.size;
  self.textContainer.lineFragmentPadding = 0;
  self.textContainer.maximumNumberOfLines = label.numberOfLines;
  self.textContainer.lineBreakMode = label.lineBreakMode;
  
  NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:label.text];
  NSRange textRange = NSMakeRange(0, attributedText.length);
  [attributedText addAttribute:NSFontAttributeName value:label.font range:textRange];
  NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
  paragraphStyle.alignment = label.textAlignment;
  [attributedText addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:textRange];
  [self.textStorage setAttributedString:attributedText];
}

根据指定的label配置我们的'View'(NSTextContainer)与'Model'(NSTextStorage)。这里要保证label是有size的,如果用了Autolayout,则要在如viewDidLayoutSubviews等layout已经完成的地方configLabel,否则textContainer接收到的size是错误的。

- (CGRect)characterRectAtIndex:(NSUInteger)charIndex
{
  if (charIndex >= self.textStorage.length) {
      NSLog(@"Plz enter a correct number");
      return CGRectZero;
  }
  NSRange characterRange = NSMakeRange(charIndex, 1);
  NSRange glyphRange = [self.layoutManager glyphRangeForCharacterRange:characterRange actualCharacterRange:nil];
  return [self.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:self.textContainer];
}

给textStorage赋值完毕后,我们就可以询问我们的'Controller',获取相应的frame。
另外提一下,给textStorage赋值完毕并布局完毕后,layoutManager便会调用以下代理方法。

  - (void)layoutManager:(NSLayoutManager *)layoutManager didCompleteLayoutForTextContainer:(nullable NSTextContainer *)textContainer atEnd:(BOOL)layoutFinishedFlag

补充

需要注意的是,刚开始我获取到的frame的x值与实际的x值总有一些误差,便猜想是我们创建出来的NSTextContainer与UILabel的布局有差异所致,所幸NSTextContainer的头文件较短,经测试,将lineFragmentPadding设为0即可。
我这里直接把label本身作为参数传了过来,其实只需要把NSTextStorage等所需的属性都赋值正确就可以正确布局。这里用到label的属性有:
bounds.size
numberOfLines
lineBreakMode
font
textAlignment

懊恼

懊恼的是,以上写法也并未达到我的期望,要是有一个自带三件套的UILabel就好了。

用TextView代替Label

UIKit还真给了一个自带三件套的'UILabel',不过它叫UITextView。
google下本文的需求,stackoverflow上就有一个答案建议使用UITextView实现。

UILabel doesn't have any methods for doing this. You can do it with UITextView, because it implements the UITextInput protocol. You will want to set the text view's editable property to NO.

核心代码为简单易懂。答主特别提到要把editable设置为NO。

- (CGRect)rectInTextView:(UITextView *)textView stringRange:(NSRange)stringRange
{
  UITextPosition *begin = [textView positionFromPosition:textView.beginningOfDocument offset:stringRange.location];
  UITextPosition *end = [textView positionFromPosition:begin offset:stringRange.length];
  UITextRange *textRange = [textView textRangeFromPosition:begin toPosition:end];
  return [textView firstRectForRange:textRange];
}

不过实际上设置同样的text,UITextView与UILabel呈现出来的样子是不一致的。万恶的罪魁是TextContainer(藏镜人不服。

  textView.scrollEnabled = NO;
  textView.scrollsToTop = NO;
  textView.editable = NO;
  textView.textContainerInset = UIEdgeInsetsZero;
  textView.textContainer.lineFragmentPadding = 0;

如上设置后,UITexView简直就是另一个UILabel了。
不过如果真的用TextView去代替Label似乎有点不太舒服,所以我Demo里的另一种做法是依然用Label做展示,seeker里用TextView做计算。

总结

平时处理文本遇到困难的话,我们可能第一时间会想到CoreText,其实来iOS7开始就有的TextKit已经提供了很多便利的方法,我们平时写UI时也该多想想是否可以用较新的API去实现,视野才能更开阔。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容