13

I've found how to set letter spacing to UILabel (here) but this method is not working for UIButtons. Does anyone know how to do it?

Here is the code i'm using

    let buttonString = agreementButton.attributedTitleForState(.Normal) as! NSMutableAttributedString
    buttonString.addAttribute(NSKernAttributeName, value: 1.0, range: NSMakeRange(0, buttonString.length))
    agreementButton.setAttributedTitle(buttonString, forState: .Normal)

It throws me an error: 'NSConcreteAttributedString' (0x19e508660) to 'NSMutableAttributedString' (0x19e506a40).

Community
  • 1
  • 1
A.Solodky
  • 171
  • 1
  • 1
  • 7

9 Answers9

19

Swift 5.0

extension UIButton{
   func addTextSpacing(_ spacing: CGFloat){
       let attributedString = NSMutableAttributedString(string: title(for: .normal) ?? "")
       attributedString.addAttribute(NSAttributedString.Key.kern, value: spacing, range: NSRange(location: 0, length: attributedString.string.count))
       self.setAttributedTitle(attributedString, for: .normal)
   }
}
button.addTextSpacing(1)

extension UILabel{
    func addTextSpacing(_ spacing: CGFloat){
        let attributedString = NSMutableAttributedString(string: self.text ?? "")
        attributedString.addAttribute(NSAttributedString.Key.kern, value: spacing, range: NSRange(location: 0, length: attributedString.string.count))
        self.attributedText = attributedString
    }
}
label.addTextSpacing(1)

extension UITextField{
    func addPlaceholderSpacing(_ spacing: CGFloat){
        let attributedString = NSMutableAttributedString(string: self.placeholder ?? "")
        attributedString.addAttribute(NSAttributedString.Key.kern, value: spacing, range: NSRange(location: 0, length: attributedString.string.count))
        self.attributedPlaceholder = attributedString
    }
}
textField.addPlaceholderSpacing(1)

extension UINavigationItem{
    func addSpacing(_ spacing: CGFloat){
        let attributedString = NSMutableAttributedString(string: self.title ?? "")
        attributedString.addAttribute(NSAttributedString.Key.kern, value: spacing, range: NSRange(location: 0, length: attributedString.string.count))
        let label = UILabel()
        label.textColor = UIColor.black
        label.font = UIFont.systemFont(ofSize: 15, weight: .bold)
        label.attributedText = attributedString
        label.sizeToFit()
        self.titleView = label
    }
}
navigationItem.addSpacing(1)
Jayaraj
  • 342
  • 3
  • 13
14
  1. Make the NSAttributedString like in the question you linked
  2. Call setAttributedTitle(_ ,forState:) on your UIButton

Try this (untested):

let title = agreementButton.titleForState(.Normal)
let attributedTitle = NSAttributedString(string: title, attributes: [NSKernAttributeName: 1.0])
agreementButton.setAttributedTitle(attributedTitle, forState: .Normal)
Code Different
  • 90,614
  • 16
  • 144
  • 163
9

Swift 4

extension UIButton{

    func addTextSpacing(_ letterSpacing: CGFloat){
        let attributedString = NSMutableAttributedString(string: (self.titleLabel?.text!)!)
        attributedString.addAttribute(NSAttributedString.Key.kern, value: letterSpacing, range: NSRange(location: 0, length: (self.titleLabel?.text!.count)!))
        self.setAttributedTitle(attributedString, for: .normal)
    }

}

// Usage: button.addTextSpacing(5.0)
Max
  • 636
  • 3
  • 13
  • 28
2

Not a full answer, but a GOTCHA, and a FIX.

GOTCHA: applying character spacing also adds the space to the END of the text. This misfeature/bug means that center-aligned text will appear incorrectly.

FIX: when creating a Range for the attributed text, subtract 1 from the text.count value (thus ignoring the last character in the string for spacing purposes.)

e.g. incorrect centering due to extra space:

enter image description here

fixed:

enter image description here

[edited]

On a related note, if you are using EdgeInsets to impose padding around the text, your UIButton subclass will need to override intrinsicContentsSize:

override open var intrinsicContentSize: CGSize {
    let size = super.intrinsicContentSize
    let insets = self.titleEdgeInsets
    let width = size.width + insets.left + insets.right
    let height = size.height + insets.top + insets.bottom
    return CGSize(width: width, height: height)
}
Womble
  • 4,607
  • 2
  • 31
  • 45
2

Swift 5 Extension goes here.

extension UIButton {
    @IBInspectable
    var letterSpacing: CGFloat {
        set {
            let attributedString: NSMutableAttributedString
            if let currentAttrString = attributedTitle(for: .normal) {
                attributedString = NSMutableAttributedString(attributedString: currentAttrString)
            }
            else {
                attributedString = NSMutableAttributedString(string: self.title(for: .normal) ?? "")
                setTitle(.none, for: .normal)
            }

            attributedString.addAttribute(NSAttributedString.Key.kern, value: newValue, range: NSRange(location: 0, length: attributedString.length))
            setAttributedTitle(attributedString, for: .normal)
        }
        get {
            if let currentLetterSpace = attributedTitle(for: .normal)?.attribute(NSAttributedString.Key.kern, at: 0, effectiveRange: .none) as? CGFloat {
                return currentLetterSpace
            }
            else {
                return 0
            }
        }
    }
}

Usage: You can set the letter space on storyboard or by code.

button.letterSpacing = 2.0
Li Jin
  • 1,879
  • 2
  • 16
  • 23
1

The solution from Code Different doesn't respect text color settings. Also one could override the UIButton class to have the spacing parameter available even in the storyboard. Here comes an updated Swift 3 solution:

Swift 3

class UIButtonWithSpacing : UIButton
{
    override func setTitle(_ title: String?, for state: UIControlState)
    {
        if let title = title, spacing != 0
        {
            let color = super.titleColor(for: state) ?? UIColor.black
            let attributedTitle = NSAttributedString(
                string: title,
                attributes: [NSKernAttributeName: spacing,
                             NSForegroundColorAttributeName: color])
            super.setAttributedTitle(attributedTitle, for: state)
        }
        else
        {
            super.setTitle(title, for: state)
        }
    }

    fileprivate func updateTitleLabel_()
    {
        let states:[UIControlState] = [.normal, .highlighted, .selected, .disabled]
        for state in states
        {
            let currentText = super.title(for: state)
            self.setTitle(currentText, for: state)
        }
    }

    @IBInspectable var spacing:CGFloat = 0 {
        didSet {
            updateTitleLabel_()
        }
    }
}
Community
  • 1
  • 1
hhamm
  • 1,511
  • 15
  • 22
1

Update for Swift 4 based off jaya raj's answer.

extension UIButton{
    func addTextSpacing(spacing: CGFloat){
        let attributedString = NSMutableAttributedString(string: (self.titleLabel?.text!)!)
        attributedString.addAttribute(NSAttributedStringKey.kern, value: spacing, range: NSRange(location: 0, length: (self.titleLabel?.text!.characters.count)!))
        self.setAttributedTitle(attributedString, for: .normal)
    }
}

extension UILabel{
    func addTextSpacing(spacing: CGFloat){
        let attributedString = NSMutableAttributedString(string: self.text!)
        attributedString.addAttribute(NSAttributedStringKey.kern, value: spacing, range: NSRange(location: 0, length: self.text!.characters.count))
        self.attributedText = attributedString
    }
}

extension UITextField{
    func addPlaceholderSpacing(spacing: CGFloat){
        let attributedString = NSMutableAttributedString(string: self.placeholder!)
        attributedString.addAttribute(NSAttributedStringKey.kern, value: spacing, range: NSRange(location: 0, length: self.placeholder!.characters.count))
        self.attributedPlaceholder = attributedString
    }
}

extension UINavigationItem{
    func addSpacing(spacing: CGFloat){
        let attributedString = NSMutableAttributedString(string: self.title!)
        attributedString.addAttribute(NSAttributedStringKey.kern, value: spacing, range: NSRange(location: 0, length: self.title!.characters.count))
        let label = UILabel()
        label.textColor = UIColor.black
        label.font = UIFont.systemFont(ofSize: 15, weight: UIFont.Weight.bold)
        label.attributedText = attributedString
        label.sizeToFit()
        self.titleView = label
    }
}
0

Update for Swift 5 without forced unwrapping:

I feel this is a better solution as we cater for existing attributes on the button as well

func addTextSpacing(_ letterSpacing: CGFloat) {
    let attributedString = attributedTitle(for: .normal)?.mutableCopy() as? NSMutableAttributedString ??
        NSMutableAttributedString(string: title(for: .normal) ?? "")
    attributedString.addAttribute(NSAttributedString.Key.kern, value: letterSpacing,
                                  range: NSRange(location: 0, length: attributedString.string.count))
    self.setAttributedTitle(attributedString, for: .normal)
}
Zaheer Moola
  • 133
  • 10
0

Swift 5

guard let buttonTitle = button.title(for: .normal) else { return }
    
    let attributedTitle = NSAttributedString(string: buttonTitle, attributes: [NSAttributedString.Key.kern: kernValue])
    button.setAttributedTitle(attributedTitle, for: .normal)
Mayur Rathod
  • 361
  • 3
  • 13