Reputation: 8661
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:
How it should look:
How can I increase the spacing between the underline and label?
Upvotes: 30
Views: 12806
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
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
}
}
Upvotes: 8
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
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