Grzegorz Aperliński
Grzegorz Aperliński

Reputation: 918

Underline covers text in NSAttributedString

I'm trying to create an attributed string but the underline covers my text instead of appearing behind it:

enter image description here

Is there a way to fix this? I'm using the following code:

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 10.0

let attributes = [NSForegroundColorAttributeName: UIColor.white,
                  NSUnderlineStyleAttributeName: NSUnderlineStyle.styleThick.rawValue,
                  NSUnderlineColorAttributeName: UIColor.red,
                  NSParagraphStyleAttributeName: paragraphStyle]

let attributedString = NSAttributedString(string: "Story", attributes: attributes)

Thanks!

EDIT:

To give more context:

I'm displaying the attributed string on a UILabel placed in a .xib file:

view.textLabel.attributedText = attributedString

The label has the following font: System Bold 32.0

I'm running the code on iPhone 6 - iOS 10.3 simulator.

EDIT 2:

I should have mentioned that the label may, at some point, contain more than one line of text. That's why the numberOfLines is set to 0.

EDIT 3: If anybody encounters this problem -- it seems that there is a lot of difference in how underline is drawn on iOS 9 vs 10 as well as UILabel vs UITextView. I've ended up having to draw the underline myself by subclassing NSLayoutManager.

Upvotes: 2

Views: 5293

Answers (5)

Gagan_iOS
Gagan_iOS

Reputation: 4080

I know its late reply but after hours search, found a fix as below

extension NSAttributedString {
    func withUnderlineSpacing(spacing: CGFloat) -> NSAttributedString {
        let attributedString = NSMutableAttributedString(attributedString: self)
        attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: NSRange(location: 0, length: self.length))
        attributedString.addAttribute(.baselineOffset, value: spacing, range: NSRange(location: 0, length: self.length))
        return attributedString
    }
}

Upvotes: 0

Victor Zaikin
Victor Zaikin

Reputation: 136

Yes, there is such problem as you have described. It shows up when you use multiline UILabel, so not only setting numberOfLines to 0, but type more than 1 line in it.

Example

let selectedStringAttributes: [String: Any]
    = [NSFontAttributeName: UIFont.boldSystemFont(ofSize: 28),
       NSForegroundColorAttributeName: UIColor.green,
       NSUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue,
       NSUnderlineColorAttributeName: UIColor.green]

let label = UILabel(frame: CGRect(x: 100, y: 100, width: 500, height: 100))
label.numberOfLines = 0

label.attributedText = NSAttributedString(string: "String to test underline", attributes: selectedStringAttributes)

And everything will look pretty good.

But if you want to use such text:

label.attributedText = NSAttributedString(string: "String to\ntest underline", attributes: selectedStringAttributes)

or label's width is too short, than:

Not so good

So the reason for such behaviour is of course bug in NSAttributedString. As it mentioned in radar there is a workaround

You should add this attribute to your NSAttributedString

NSBaselineOffsetAttributeName: 0

And magic will happen.

Upvotes: 6

ironRoei
ironRoei

Reputation: 2219

So this is my solution to this issue. I think it is "cleaner" and easier. Post me if you dont understand :)

class BottomLineTextField: UITextField {

    var bottomBorder = UIView()

    override func awakeFromNib() {
        super.awakeFromNib()
        setBottomBorder()
    }

    func setBottomBorder() {            
        self.translatesAutoresizingMaskIntoConstraints = false

        bottomBorder = UIView.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        hasError = false
        bottomBorder.translatesAutoresizingMaskIntoConstraints = false

        addSubview(bottomBorder)

        bottomBorder.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        bottomBorder.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
        bottomBorder.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
        bottomBorder.heightAnchor.constraint(equalToConstant: 1).isActive = true // Set underline height
    }
}

Upvotes: 2

matt
matt

Reputation: 535944

On my machine, showing your attributed string in a black-backgrounded UILabel, it makes a quite nice-looking display:

enter image description here

The red thick underline is nicely separated from the text, and is interrupted to allow the descender of the "y" to pass through it.

NOTE You cannot combine the font of the UILabel (set in Interface Builder) with its attributedText. You must set the entire label's text formatting in the attributedText. So, my code looks like this:

    let attributes : [String:Any] = [NSForegroundColorAttributeName: UIColor.white,
                      NSUnderlineStyleAttributeName: NSUnderlineStyle.styleThick.rawValue,
                      NSUnderlineColorAttributeName: UIColor.red,
                      NSFontAttributeName: UIFont.boldSystemFont(ofSize: 32)]
    let attributedString = NSAttributedString(string: "Story", attributes: attributes)
    lab.backgroundColor = .black
    lab.attributedText = attributedString

(You will notice that I removed your stipulation of the paragraph line spacing; there is only one line, so this stipulation adds nothing. However, I get the same result even if I restore it.)

Upvotes: 3

RajeshKumar R
RajeshKumar R

Reputation: 15778

Instead of using NSAttributedString you can draw border below the label with x space using this.

let space:CGFloat = 10

let border = CALayer()
border.backgroundColor = UIColor.red.cgColor
border.frame = CGRect(x: 0, y: (label?.frame.size.height)! + space, width: (label?.frame.size.width)!, height: 1)
label?.layer.addSublayer(border)

Upvotes: 2

Related Questions