How to define a custom NSUnderlineStyle
Asked Answered
B

1

7

Looking at the documentation for NSLayoutManager, specifically the drawUnderlineForGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin: method, I noticed the following (emphasis mine):

underlineVal
The style of underlining to draw. This value is a mask derived from the value for NSUnderlineStyleAttributeName—for example, (NSUnderlinePatternDash | NSUnderlineStyleThick). Subclasses can define custom underlining styles.

My question is: how exactly is that meant to be done?

NSUnderlineStyle is an enum, which you cannot extend or override. You can of course provide a random raw Int value for the attribute, not covered by the enum cases:

self.addAttribute(NSUnderlineStyleAttributeName, value: 100022, range: lastUpdatedWordRange)

Which will deliver an "invalid" but usable underlineType to the Layout Manger:

debugger screenshot

But this hardly feels safe and is definitely inelegant.

I was not able to find any examples online or further clues in Apple documentation on what those mythical custom underline style types look like. I'd love to know if I'm missing something obvious.

Botfly answered 25/3, 2016 at 11:4 Comment(1)
NSUnderlineStyle seems to be a poor team player. It's poorly documented, and, in Swift, it still hasn't been converted to be an option set. When reading the flags in a given style, it seems to be unreliable (or else I'm doing something wrong). Wish I could help more, but I'm still investigating a solution...Zymotic
E
5

I have an example project here that I used for a talk on TextKit that I gave a while back that does exactly what you're looking for: https://github.com/dtweston/text-kit-example

The underline in this case is a squiggly line:

underline screenshot

The meat of the solution is a custom NSLayoutManager:

let CustomUnderlineStyle = 0x11

class UnderlineLayoutManager: NSLayoutManager {
    
    func drawFancyUnderlineForRect(_ rect: CGRect) {
        let left = rect.minX
        let bottom = rect.maxY
        let width = rect.width
        
        let path = UIBezierPath()
        path.move(to: CGPoint(x: left, y: bottom))
        var x = left
        var y = bottom
        var i = 0
        while (x <= left + width) {
            path.addLine(to: CGPoint(x: x, y: y))
            x += 2
            if i % 2 == 0 {
                y = bottom + 2.0
            }
            else {
                y = bottom
            }
            i += 1;
        }
        
        path.stroke()
    }
    
    override func drawUnderline(forGlyphRange glyphRange: NSRange, underlineType underlineVal: NSUnderlineStyle, baselineOffset: CGFloat, lineFragmentRect lineRect: CGRect, lineFragmentGlyphRange lineGlyphRange: NSRange, containerOrigin: CGPoint) {
        
        if underlineVal.rawValue & CustomUnderlineStyle == CustomUnderlineStyle {
            
            let charRange = characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
            if let underlineColor = textStorage?.attribute(NSUnderlineColorAttributeName, at: charRange.location, effectiveRange: nil) as? UIColor {
                underlineColor.setStroke()
            }
            
            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)
                
                drawFancyUnderlineForRect(offsetRect)
            }
        }
        else {
            super.drawUnderline(forGlyphRange: glyphRange, underlineType: underlineVal, baselineOffset: baselineOffset, lineFragmentRect: lineRect, lineFragmentGlyphRange: lineGlyphRange, containerOrigin: containerOrigin)
        }
    }
}
Ethiop answered 15/1, 2017 at 8:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.