Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Underline text - line thickness in crossing ranges with different font sizes

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:

enter image description here

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:

enter image description here

How can I do this using TextKit?

like image 584
xexe Avatar asked Jan 12 '23 04:01

xexe


1 Answers

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);
}

sample

like image 97
Emmanuel Avatar answered Jan 24 '23 16:01

Emmanuel