Underline text - line thickness in crossing ranges with different font sizes
ios, nsattributedstring, textkit, uitextview
Solution
On iOS 7 you can do what you want with a subclass of `NSLayoutManager` that override `-drawUnderlineForGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:` A little sample that create text system objects, and draw the wanted underline.
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// setup text handling with our subclass of NSLayoutManager
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:@"Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do;"];
MyLayoutManager *textLayout = [[MyLayoutManager alloc] init];
[textStorage addLayoutManager:textLayout];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.view.bounds.size];
[textLayout addTextContainer:textContainer];
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0,20,self.view.bounds.size.width,self.view.bounds.size.height-20)
textContainer:textContainer];
[self.view addSubview:textView];
// setup test attributes
UIFont *font = [UIFont fontWithName:@"TimesNewRomanPSMT" size:15];
NSMutableDictionary *attributesToSetDict = [NSMutableDictionary dictionary];
[attributesToSetDict setObject:font forKey:NSFontAttributeName];
[attributesToSetDict setObject:[UIColor blueColor] forKey:NSForegroundColorAttributeName];
[attributesToSetDict setObject:@1 forKey:NSUnderlineStyleAttributeName];
[textView.textStorage setAttributes:attributesToSetDict range:NSMakeRange(9, 28)];
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
attributesToSetDict = [NSMutableDictionary dictionary];
[attributesToSetDict setObject:[UIFont fontWithDescriptor:fontDescriptor size:25] forKey:NSFontAttributeName];
[attributesToSetDict setObject:[UIColor redColor] forKey:NSForegroundColorAttributeName];
[attributesToSetDict setObject:@1 forKey:NSUnderlineStyleAttributeName];
[textView.textStorage setAttributes:attributesToSetDict range:NSMakeRange(26, 7)];
}
@end
@implementation MyLayoutManager
- (void)drawUnderlineForGlyphRange:(NSRange)glyphRange underlineType:(NSUnderlineStyle)underlineVal baselineOffset:(CGFloat)baselineOffset lineFragmentRect:(CGRect)lineRect lineFragmentGlyphRange:(NSRange)lineGlyphRange containerOrigin:(CGPoint)containerOrigin
{
/* For the sample we consider that underlineVal is equals to NSUnderlineStyleSingle */
/*
* We need to create a CTFontRef as UIFont doesn't provided 'Underline Position' and 'Underline Thickness'
*/
// get current UIFont for the glyphRange
NSRange characterRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
UIFont *font = [[self.textStorage attributesAtIndex:characterRange.location effectiveRange:NULL] objectForKey:NSFontAttributeName];
CTFontRef ctfont = CTFontCreateWithName((CFStringRef)[font fontName], [font pointSize], NULL);
CGPoint startLocation = [self locationForGlyphAtIndex:glyphRange.location];
CGPoint endLocation = [self locationForGlyphAtIndex:NSMaxRange(glyphRange)];
CGContextRef ctx = UIGraphicsGetCurrentContext();
// -underlineGlyphRange:underlineType:lineFragmentRect:lineFragmentGlyphRange:containerOrigin: already set color and line width
// for the current context
// reset line width with current font underline thickness
CGContextSetLineWidth(ctx, CTFontGetUnderlineThickness(ctfont));
CGContextMoveToPoint(ctx, startLocation.x + containerOrigin.x, startLocation.y + containerOrigin.y - CTFontGetUnderlinePosition(ctfont));
CGContextAddLineToPoint(ctx, endLocation.x + containerOrigin.x, endLocation.y + containerOrigin.y - CTFontGetUnderlinePosition(ctfont));
CGContextStrokePath(ctx);
CFRelease(ctfont);
}
Problem
I am working with the attributes of textStorage of `UITextView`. I have the string and an array of the objects of my class `TextFormattingElement`. An instance of this class consists of `NSRange` (on which this element has to be applied in text) and some formatting parameters: ``` @interface TextFormattingElement : NSObject @property (nonatomic) NSRange range; @property (nonatomic, strong) NSString *fontName; //e.g. @"TimesNewRomanPSMT" @property (nonatomic) int fontSize; @property (nonatomic, strong) UIColor *fontColor; @property (nonatomic) BOOL isBold; @property (nonatomic) BOOL isItalic; @property (nonatomic) BOOL isUnderlined; @property (nonatomic) BOOL isStriked; @end ``` Now i loop through this array and successively apply this elements to the textStorage of textView. I use this method: ``` -(void)setFontWithName:(NSString*)name AndSize:(float)fontSize AndTextColor:(UIColor*)textColor AndIsBold:(BOOL)isBold AndIsItalic:(BOOL)isItalic AndIsUnderlined:(BOOL)isUnderLined andIsStriked:(BOOL)isStriked ToRange:(NSRange)rangeToSet{ __block UIFont *font = [UIFont fontWithName:name size:fontSize]; __block UIFontDescriptor *fontDescriptor = [font fontDescriptor]; [textView.textStorage enumerateAttributesInRange:rangeToSet options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { NSParagraphStyle *paragraphStyle = [attrs objectForKey:NSParagraphStyleAttributeName]; NSMutableDictionary *attributesToSetDict = [NSMutableDictionary dictionary]; [attributesToSetDict setObject:paragraphStyle forKey:NSParagraphStyleAttributeName]; //i need to clear all attributes at this range exсept NSParagraphStyleAttributeName if(isBold){ uint32_t existingTraitsWithNewTrait = [fontDescriptor symbolicTraits] | UIFontDescriptorTraitBold; fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:existingTraitsWithNewTrait]; } if(isItalic){ uint32_t existingTraitsWithNewTrait = [fontDescriptor symbolicTraits] | UIFontDescriptorTraitItalic; fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:existingTraitsWithNewTrait]; } font = [UIFont fontWithDescriptor:fontDescriptor size:fontSize]; [attributesToSetDict setObject:font forKey:NSFontAttributeName]; [attributesToSetDict setObject:textColor forKey:NSForegroundColorAttributeName]; if(isUnderLined){ [attributesToSetDict setObject:@1 forKey:NSUnderlineStyleAttributeName]; } if(isStriked){ //TODO: isStriked } [textView.textStorage setAttributes:attributesToSetDict range:range]; }]; } ``` I have one problem: if i have two `TextFormattingElement` instances with crossing ranges (e.g. `NSMakeRange(9,28)` and `NSMakeRange(26,7)`) the thickness of underline has always the value dependent of the last element's font size. The illustration of this you can see at this screenshot: The parameters of my 2 formatting elements are: 1st: location = 9, length = 28, fontName = TimesNewRomanPSMT, fontSize = 15, fontColor = UIDeviceRGBColorSpace 1 0 0 1, isBold = 0, isItalic = 0, isUnderlined = 1, isStriked = 0 2nd: location = 26, length = 7, fontName = TimesNewRomanPSMT, fontSize = 25, fontColor = UIDeviceRGBColorSpace 0 0 1 1, isBold = 1, isItalic = 0, isUnderlined = 1, isStriked = 0 But I want to get an effect like in Google Docs: How can I do this using TextKit?