Customize underline pattern in NSAttributedString (iOS7 +)

I am trying to add a dotted underline style to NSAttributedString

(on iOS7 + / TextKit). Of course, I tried the built-in NSUnderlinePatternDot

:

NSString *string = self.label.text;
NSRange underlineRange = [string rangeOfString:@"consetetur sadipscing elitr"];

NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:string];
[attString addAttributes:@{NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot)} range:underlineRange];

self.label.attributedText = attString;

      

However, the style created by this is actually quite broken than the dotted line:

screenshot

Am I missing something obvious here ( NSUnderlinePatternReallyDotted

?;)) Or is there a way to customize the dot-pattern?

+5


source to share


3 answers


I spent a little time playing around with the Text Kit to get this and other similar scripts to work. The actual Text Kit solution for this problem is to subclass NSLayoutManager

and overridedrawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:)

This is the method that is called to actually draw the underline requested by the attributes in NSTextStorage

. Here's a description of the parameters that are passed to:

  • glyphRange

    the index of the first glyph and the number of next glyphs to be underlined
  • underlineType

    NSUnderlineStyle

    attribute valueNSUnderlineAttributeName

  • baselineOffset

    the distance between the bottom border of the box and the baseline offset for glyphs in the provided range
  • lineFragmentRect

    a rectangle that spans the entire line containing glyphRange

  • lineFragmentGlyphRange

    a range of glyphs that makes up the entire line containing glyphRange

  • containerOrigin

    the offset of the container in the text view to which it belongs.


Steps to underline:

  • Find NSTextContainer

    which contains glyphRange

    using NSLayoutManager.textContainer(forGlyphAt:effectiveRange:)

    .
  • Get bounding rectangle for glyphRange

    usingNSLayoutManager.boundingRect(forGlyphRange:in:)

  • Nudge the bounding rectangle with the source of the text container using CGRect.offsetBy(dx:dy:)

  • Draw your custom underline somewhere between the bottom edge of the offset bounding box and the baseline (as defined baselineOffset

    )
+2


source


According to Eiko & Dave's answer, I made an example like this

I tried to end this with a UILabel and not a UITextView but couldn't find a solution after searching from stackoverflow or other websites. So, I used UITextView to do this.

    let storage = NSTextStorage()
    let layout = UnderlineLayout()
    storage.addLayoutManager(layout)
    let container = NSTextContainer()
    container.widthTracksTextView = true
    container.lineFragmentPadding = 0
    container.maximumNumberOfLines = 2
    container.lineBreakMode = .byTruncatingTail
    layout.addTextContainer(container)

    let textView = UITextView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width-40, height: 50), textContainer: container)
    textView.isUserInteractionEnabled = true
    textView.isEditable = false
    textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    textView.text = "1sadasdasdasdasdsadasdfdsf"
    textView.backgroundColor = UIColor.white

    let rg = NSString(string: textView.text!).range(of: textView.text!)
    let attributes = [NSAttributedString.Key.underlineStyle.rawValue: 0x11,
                      NSAttributedString.Key.underlineColor: UIColor.blue.withAlphaComponent(0.2),
                      NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17),
                      NSAttributedString.Key.baselineOffset:10] as! [NSAttributedString.Key : Any]

    storage.addAttributes(attributes, range: rg)
    view.addSubview(textView)

      



override LayoutManage method

class UnderlineLayout: NSLayoutManager {
    override func drawUnderline(forGlyphRange glyphRange: NSRange, underlineType underlineVal: NSUnderlineStyle, baselineOffset: CGFloat, lineFragmentRect lineRect: CGRect, lineFragmentGlyphRange lineGlyphRange: NSRange, containerOrigin: CGPoint) {
    if let container = textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil) {
        let boundingRect = self.boundingRect(forGlyphRange: glyphRange, in: container)
        let offsetRect = boundingRect.offsetBy(dx: containerOrigin.x, dy: containerOrigin.y)

        let left = offsetRect.minX
        let bottom = offsetRect.maxY-15
        let width = offsetRect.width
        let path = UIBezierPath()
        path.lineWidth = 3
        path.move(to: CGPoint(x: left, y: bottom))
        path.addLine(to: CGPoint(x: left + width, y: bottom))
        path.stroke()
    }
}

      

In my solution, I have to extend the lineSpacing and also keep the setting underline using the NSAttributedString NSMutableParagraphStyle () property. lineSpacing, but it didn't seem to work, but NSAttributedString.Key.baselineOffset does. hope may

+1


source


I know this is a two year old story, but maybe it will help someone facing the same problem as last week. My workaround was to subclass UITextView

, add a subview (container with dashed lines) and use the textViews layoutManager method enumerateLineFragmentsForGlyphRange

to get the number of lines and their frames. Knowing the frame of the lines, I figured y

out where I needed the points. So I created a view (the lineView in which I drew the dots), set clipsToBounds

to YES

, width to width of the text and then added as a subhead in the LineContainer while doing so y

. After that, I set lineViews width

to return valueenumerateLineFragmentsForGlyphRange

... Multiple rows use the same approach: update frames, add new rows, or delete according to what it returns enumerateLineFragmentsForGlyphRange

. This method is called textViewDidChange

after every text update. Here is the code to get an array of strings and their frames

NSLayoutManager *layoutManager = [self layoutManager];
[layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, [layoutManager numberOfGlyphs]) 
                                        usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
                                                        [linesFramesArray addObject:NSStringFromCGRect(usedRect)];
                                                   }
];

      

0


source







All Articles