MobileMon
MobileMon

Reputation: 8661

NSUnderlineStyleAttributeName Underline spacing

I want the underline to be below the text like any other normal underlining mechanism. However, with NSAttributed string it leaves holes with "g's" and "y's"

example: enter image description here

How it should look: enter image description here

How can I increase the spacing between the underline and label?

Upvotes: 30

Views: 12806

Answers (4)

JonKevinOlivet
JonKevinOlivet

Reputation: 21

Ferran Maylinch's answer worked the best for me.

I turned it into an UILabel extension that I thought I'd share.

extension UILabel {

func addUnbrokenUnderline() {
    let line = UIView()
    line.translatesAutoresizingMaskIntoConstraints = false
    line.backgroundColor = textColor
    addSubview(line)
    NSLayoutConstraint.activate([
        line.heightAnchor.constraint(equalToConstant: 1.0),
        line.leadingAnchor.constraint(equalTo: leadingAnchor),
        line.widthAnchor.constraint(equalToConstant: intrinsicContentSize.width),
        line.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5.0)
    ])
}

}

Upvotes: 2

Dmitry Rybochkin
Dmitry Rybochkin

Reputation: 81

You could use UITextView I added custom NSAttributedStringKey "customUnderline" and swizzling method drawUnderline in NSLayoutManager.

import Foundation
import SwiftyAttributes
import UIKit

private let swizzling: (AnyClass, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in
    guard let originalMethod = class_getInstanceMethod(forClass, originalSelector),
        let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) else {
            return
    }
    method_exchangeImplementations(originalMethod, swizzledMethod)
}

extension NSAttributedStringKey {
    static var customUnderline: NSAttributedStringKey = NSAttributedStringKey("customUnderline")
}

extension Attribute {
    static var customUnderline: Attribute = Attribute.custom(NSAttributedStringKey.customUnderline.rawValue, true)
}

extension NSLayoutManager {

    // MARK: - Properties

    static let initSwizzling: Void = {
        let originalSelector = #selector(drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:))
        let swizzledSelector = #selector(swizzled_drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:))
        swizzling(NSLayoutManager.self, originalSelector, swizzledSelector)
    }()

    // MARK: - Functions

    @objc
    func swizzled_drawUnderline(forGlyphRange glyphRange: NSRange, underlineType underlineVal: NSUnderlineStyle, baselineOffset: CGFloat, lineFragmentRect lineRect: CGRect, lineFragmentGlyphRange lineGlyphRange: NSRange, containerOrigin: CGPoint) {
        guard needCustomizeUnderline(underlineType: underlineVal) else {
            swizzled_drawUnderline(forGlyphRange: glyphRange,
                                   underlineType: underlineVal,
                                   baselineOffset: baselineOffset,
                                   lineFragmentRect: lineRect,
                                   lineFragmentGlyphRange: lineGlyphRange,
                                   containerOrigin: containerOrigin)
            return
        }

        let heightOffset = containerOrigin.y - 1 + (getFontHeight(in: glyphRange) ?? (lineRect.height / 2))
        drawStrikethrough(forGlyphRange: glyphRange,
                          strikethroughType: underlineVal,
                          baselineOffset: baselineOffset,
                          lineFragmentRect: lineRect,
                          lineFragmentGlyphRange: lineGlyphRange,
                          containerOrigin: CGPoint(x: containerOrigin.x,
                                                   y: heightOffset))
    }

    // MARK: - Private functions

    private func needCustomizeUnderline(underlineType underlineVal: NSUnderlineStyle) -> Bool {
        guard underlineVal == NSUnderlineStyle.styleSingle else {
            return false
        }
        let attributes = textStorage?.attributes(at: 0, effectiveRange: nil)
        guard let isCustomUnderline = attributes?.keys.contains(.customUnderline), isCustomUnderline else {
            return false
        }
        return true
    }

    private func getFontHeight(in glyphRange: NSRange) -> CGFloat? {
        let location = characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil).location
        guard let font = textStorage?.attribute(.font, at: location, effectiveRange: nil) as? UIFont else {
            return nil
        }
        return font.capHeight
    }
}

like that

Upvotes: 8

Ferran Maylinch
Ferran Maylinch

Reputation: 11539

I added a line (UIView) with height 1 and width like label, aligned to the bottom of the UILabel.

    let label = UILabel()
    label.text = "underlined text"

    let spacing = 2 // will be added as negative bottom margin for more spacing between label and line

    let line = UIView()
    line.translatesAutoresizingMaskIntoConstraints = false
    line.backgroundColor = label.textColor
    label.addSubview(line)
    label.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[line]|", metrics: nil, views: ["line":line]))
    label.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[line(1)]-(\(-spacing))-|", metrics: nil, views: ["line":line]))

Upvotes: 8

DarkDust
DarkDust

Reputation: 92384

There is no way to control that behaviour with NSAttributedString or CoreText (apart from drawing the underline yourself). NSAttributedString has no option for that (and CoreText hasn't got one, either).

On Apple systems, the first version (with the gap) is the "expected" behaviour as it's the one Apple provides and is used throughout the system (and apps like Safari, TextEdit, etc.).

If you really, really want to have underlines without a gap, you need to draw the string without an underline and draw the line yourself (which I needed to do in one of my projects and I can tell you it's hard; see this file, search for "underline").

Upvotes: 25

Related Questions