NSAttributedString reporting incorrect sizes for U

2020-06-12 07:02发布

问题:

I have an NSAttributedString that is reporting a boundingRectWithSize (and by extension a UITextView which improperly calculates its sizeThatFits) when the font size is decreased from the font size that was used to create it.

It doesn't happen on all NSAttributedStrings for which I do similar operations, so here's the steps to reproduce.

  1. Use a non-standard font that does not include the full unicode character set.
  2. Make sure the string includes characters in this "unsupported" set. iOS will render them as Helvetica in the proper size.
  3. Scale your font down on all font attributes in your NSAttributedString. My code for doing so that produced the issue looks like this.

From inside a UITextView subclass:

NSMutableAttributedString *mutableString = [self.attributedText mutableCopy];
[mutableString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, mutableString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
    if (value) {
        UIFont *oldFont = (UIFont *)value;
        UIFont *newFont = [oldFont fontWithSize:oldFont.pointSize - 1];
        [mutableString removeAttribute:NSFontAttributeName range:range];
        [mutableString addAttribute:NSFontAttributeName value:newFont range:range];
    }
}];
self.attributedText = [mutableString copy];

I noticed that while running this code in a while loop checking sizeThatFits to know when the text is small enough to fit that I would have a race to zero occur in some circumstances. The height is being calculated as 60px for any font value smaller than what I started with, which happens to be 50px.

When NSLoging the NSAttributedString I find that there are several attributes that I did not add with the key NSOriginalFont which does not appear to be in the list of supported attributes here. What's going on with NSOriginalFont? Why is my size being calculated incorrectly?

回答1:

I ended up fixing this but found a lack of information on the web about it, so I decided to document my solution here.

NSOriginalFont attributes are created when the font used doesn't support one or more characters in the string. NSAttributedString adds these attributes that track what the font was "supposed" to be before a substitution to Helvetica occurred. I could make up a situation where this is useful (an all-caps font that you sometimes run uppercaseString: on?) but it wasn't useful to me.

In fact it was harmful. As I iterated through my font related attributes to decrease the size as shown above the visible size of the text was decreasing but the NSOriginalFont attribute retained a reference to the large size.

There's no built in constant for NSOriginalFont but if you call it by name it's possible to strip it from your NSMutableAttributedString. If you do you'll begin to get proper results from sizeThatFits, boundingRectWithSize, and other similar functions assuming that you're passing the correct options.

I ended up creating a simple category method on NSMutableAttributedString, included below, that works well.

NSMutableAttributedString+StripOriginalFont.h

@interface NSMutableAttributedString (StripOriginalFont)

- (void) stripOriginalFont;

@end

NSMutableAttributedString+StripOriginalFont.m

@implementation NSMutableAttributedString (StripOriginalFont)

- (void) stripOriginalFont{
    [self enumerateAttribute:@"NSOriginalFont" inRange:NSMakeRange(0, self.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
        if (value){
            [self removeAttribute:@"NSOriginalFont" range:range];
        }
    }];
}

@end

Presumably you could simply modify it to keep it "in-sync" instead of removing it entirely but it wasn't useful to me for this particular project.



回答2:

Don't know if this will help solve your issue, but check out my solution for auto-sizing text UITextView here: https://stackoverflow.com/a/30400391/1664123



回答3:

Create NSTextStorage object and init with the attributedString. and calculate bounds.

NSTextStorage *attributedText = [[NSTextStorage alloc] initWithAttributedString:[[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName:systemFont}]];
CGRect textRect = [attributedText boundingRectWithSize:CGSizeMake(textW, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil];


回答4:

For stripping in swift you can use this extension:

extension NSAttributedString {
  func strippedOriginalFont() -> NSAttributedString? {
    let mutableCopy = self.mutableCopy() as? NSMutableAttributedString
    mutableCopy?.removeAttribute(NSAttributedStringKey(rawValue: "NSOriginalFont"), range: NSMakeRange(0, self.length))
    return mutableCopy?.copy() as? NSAttributedString
  }
}


回答5:

I had the same problem. when I call textView.setAttributedString(), it will automatically add NSOriginalFont attribute for me which lead to the wrong size. I also use sizeThatFits to calculate height.

The reason we got the wrong size is textView use the NSOriginalFont to calculate size which is not suitable for changed NSFont.

But if we use a NSTextStorage to create the attributedString and call textView.setAttributedText, then it will not add NSOriginalFont(I don't know why, but this fixes my problem) and the calculation of size will get the right answer.

Simple Code:

func getAttributedStringForTextView(content: String) -> NSAttributedString {
    var attriString = NSMutableAttributedString(string: content)
    // add attributes here
    ...
    // at last, use an NSTextStorage to wrap the result
    return NSTextStorage(attributedString: attriString)
}

Hope this helps.