2

Some users may set the "Button Shapes" switch On. (Settings -> Accessibility -> Display & Text Size -> Button Shapes)

This causes the titles of buttons is automatically underlined. It's terrible in some eases such as this screen shot:

Is possible to override it as Off programmatilly? I use Swift.

Thanks in advance.

denis_lor
  • 6,212
  • 4
  • 31
  • 55
michaelma
  • 90
  • 2
  • 9
  • I don't believe you can "override it as Off" ... You *can* set the attributed title and explicitly set `NSAttributedString.Key.underlineStyle, value: 0` for each button. However... users who toggle `Button Shapes -> ON` do so because they ***want*** the underlines to be visible. It seems more likely they would complain about **not** seeing them rather than the other way around. – DonMag Mar 06 '20 at 18:55
  • @DonMag, thanks for your comment. I have not tried your method. But someone reported it would not work.[link](https://stackoverflow.com/questions/34535241/swift-uibutton-how-to-remove-underline?rq=1) I notice that the system calculator of iPhone does not be affected by the "Button Shapes" setting. Maybe it use image in its buttons(Not a text title)? – michaelma Mar 07 '20 at 01:09
  • Actually, @DonMag, I first started using this feature because I wanted button SHAPES as the feature name implies. Unfortunately, starting with iOS 11, Apple changed the rendering from button shapes to underlining. The underlining looks horrible, IMO, compared to the previous implementation that rendered the button with a shape. Unfortunately, there doesn't seem to be a way to get it to render the old way anymore. – Victor Engel Dec 26 '20 at 07:42

1 Answers1

3

Here is a simple example of using .setAttributedTitle() to remove the underline applied by Accessibility -> Button Shapes.

Note that this is just quickly slapped together -- I have not done testing on it, so don't consider it "production" code. Also note that the code is very "show how to do it", not "this is a good way to do it".

class RoundButton: UIButton {
    override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = bounds.height * 0.5
    }
}

class ButtonShapesViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor(red: 54.0 / 255.0, green: 15.0 / 255.0, blue: 30.0 / 255.0, alpha: 1.0)

        // just for easy layout / quick testing
        // add a vertical stackView holding two horizontal stackViews for two rows of buttons
        let svTopRow = UIStackView()
        svTopRow.axis = .horizontal
        svTopRow.alignment = .fill
        svTopRow.distribution = .fill
        svTopRow.spacing = 8
        svTopRow.translatesAutoresizingMaskIntoConstraints = false

        let svBotRow = UIStackView()
        svBotRow.axis = .horizontal
        svBotRow.alignment = .fill
        svBotRow.distribution = .fill
        svBotRow.spacing = 8
        svBotRow.translatesAutoresizingMaskIntoConstraints = false

        let svRows = UIStackView()
        svRows.axis = .vertical
        svRows.alignment = .fill
        svRows.distribution = .fill
        svRows.spacing = 8
        svRows.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(svRows)

        svRows.addArrangedSubview(svTopRow)
        svRows.addArrangedSubview(svBotRow)

        NSLayoutConstraint.activate([
            svRows.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            svRows.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])

        // fill "row" stackViews each with 4 buttons
        [svTopRow, svBotRow].forEach {

            sv in

            ["7", "8", "9", "÷"].forEach {

                title in

                let btn = RoundButton()
                btn.backgroundColor = UIColor(red: 201.0 / 255.0, green: 59.0 / 255.0, blue: 114.0 / 255.0, alpha: 1.0)
                btn.titleLabel?.font = UIFont.systemFont(ofSize: 28.0)
                btn.setTitleColor(.white, for: .normal)
                btn.setTitleColor(.lightGray, for: .highlighted)
                btn.setTitle(title, for: .normal)
                btn.widthAnchor.constraint(equalToConstant: 54.0).isActive = true
                btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true

                sv.addArrangedSubview(btn)

            }

        }

        // now, let's disable the "Button Shapes" underlining for the bottom row

        svBotRow.arrangedSubviews.forEach {
            // just for sanity
            guard let btn = $0 as? UIButton, let title = btn.currentTitle, let curAttText = btn.titleLabel?.attributedText  else {
                fatalError("Not a button!")
            }
            // normal state
            let mutAttTextNorm = NSMutableAttributedString(attributedString: curAttText)
            mutAttTextNorm.addAttribute(NSAttributedString.Key.underlineStyle, value: 0, range: NSRange(location: 0, length: title.count))
            btn.setAttributedTitle(mutAttTextNorm, for: .normal)
            // highlighted state
            let mutAttTextHigh = NSMutableAttributedString(attributedString: curAttText)
            mutAttTextHigh.addAttribute(NSAttributedString.Key.underlineStyle, value: 0, range: NSRange(location: 0, length: title.count))
            mutAttTextHigh.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.lightGray, range: NSRange(location: 0, length: title.count))
            btn.setAttributedTitle(mutAttTextHigh, for: .highlighted)
        }

    }

}

Result (bottom row has underline disabled):

enter image description here

The philosophical discussion of whether or not to actually do this is out-of-scope for Stack Overflow. As you mentioned in your comment, Calculator buttons don't get underlines... also Calendar app "round day buttons", Search button and "+" Add buttons don't show underlines.


EDIT

Here is another example -- the "5 x 5 grid" of buttons resizes on device rotation.

Button taps simply append their title to the "inputLabel" - no calculations are being made. Except for the "backspace key" button which toggles between "⌫" unselected and "⌦" selected states.

class RoundButton: UIButton {
    override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = bounds.height * 0.5
    }
}

struct CalcButton {
    var normalTitle: String = ""
    var selectedTitle: String = ""
    var foreColor: UIColor = .white
    var backColor: UIColor = .black
}

class ButtonShapesViewController: UIViewController {

    let inputLabel: UILabel = {
        let v = UILabel()
        v.textColor = .white
        v.textAlignment = .right
        v.text = "0"
        v.font = UIFont.systemFont(ofSize: 28.0)
        v.translatesAutoresizingMaskIntoConstraints = false
        v.setContentHuggingPriority(.required, for: .vertical)
        v.setContentCompressionResistancePriority(.required, for: .vertical)
        return v
    }()

    var normAttributes: [NSAttributedString.Key: Any] = [:]
    var highAttributes: [NSAttributedString.Key: Any] = [:]

    let colorA: UIColor = UIColor(red: 254.0 / 255.0, green:  76.0 / 255.0, blue: 144.0 / 255.0, alpha: 1.0)
    let colorB: UIColor = UIColor(red: 201.0 / 255.0, green:  60.0 / 255.0, blue: 114.0 / 255.0, alpha: 1.0)
    let colorC: UIColor = UIColor(red: 196.0 / 255.0, green:  31.0 / 255.0, blue:  58.0 / 255.0, alpha: 1.0)
    let colorD: UIColor = UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0)

    let fColorA: UIColor = UIColor(red:  53.0 / 255.0, green:  15.0 / 255.0, blue:  30.0 / 255.0, alpha: 1.0)
    let fColorD: UIColor = UIColor(red: 197.0 / 255.0, green:  36.0 / 255.0, blue:  61.0 / 255.0, alpha: 1.0)

    var padButtons: [CalcButton] = [CalcButton]()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor(red: 54.0 / 255.0, green: 15.0 / 255.0, blue: 30.0 / 255.0, alpha: 1.0)

        padButtons = [
            CalcButton(normalTitle: "←",  selectedTitle: "", foreColor: fColorA, backColor: colorA),
            CalcButton(normalTitle: "→",  selectedTitle: "", foreColor: fColorA, backColor: colorA),
            CalcButton(normalTitle: "(",  selectedTitle: "", foreColor: fColorA, backColor: colorA),
            CalcButton(normalTitle: "()", selectedTitle: "", foreColor: fColorA, backColor: colorA),
            CalcButton(normalTitle: ")",  selectedTitle: "", foreColor: fColorA, backColor: colorA),

            CalcButton(normalTitle: "7",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "8",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "9",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "÷",  selectedTitle: "", foreColor: .white, backColor: colorC),
            CalcButton(normalTitle: "AC", selectedTitle: "", foreColor: fColorD, backColor: colorD),

            CalcButton(normalTitle: "4",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "5",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "6",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "x",  selectedTitle: "", foreColor: .white, backColor: colorC),
            CalcButton(normalTitle: "⌫",  selectedTitle: "⌦", foreColor: fColorD, backColor: colorD),

            CalcButton(normalTitle: "1",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "2",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "3",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "-",  selectedTitle: "", foreColor: .white, backColor: colorC),
            CalcButton(normalTitle: "↖︎↘︎", selectedTitle: "", foreColor: fColorD, backColor: colorD),

            CalcButton(normalTitle: "0",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: ".",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "π",  selectedTitle: "", foreColor: .white, backColor: colorB),
            CalcButton(normalTitle: "+",  selectedTitle: "", foreColor: .white, backColor: colorC),
            CalcButton(normalTitle: "✓",  selectedTitle: "", foreColor: fColorD, backColor: colorD),
        ]

        let svRows = UIStackView()
        svRows.axis = .vertical
        svRows.alignment = .fill
        svRows.distribution = .fillEqually
        svRows.spacing = 8
        svRows.translatesAutoresizingMaskIntoConstraints = false

        let font = UIFont.systemFont(ofSize: 28.0)

        var idx = 0

        for _ in 1...5 {

            let svRow = UIStackView()
            svRow.axis = .horizontal
            svRow.alignment = .fill
            svRow.distribution = .fillEqually
            svRow.spacing = 8
            svRow.translatesAutoresizingMaskIntoConstraints = false

            for _ in 1...5 {

                let cb: CalcButton = padButtons[idx]

                let btn = RoundButton()
                btn.backgroundColor = cb.backColor

                normAttributes = [
                    .foregroundColor : cb.foreColor,
                    .underlineStyle: 0,
                    .font : font,
                ]

                highAttributes = [
                    .foregroundColor : UIColor.lightGray,
                    .underlineStyle: 0,
                    .font : font,
                ]

                let attNorm = NSAttributedString(string: cb.normalTitle, attributes: normAttributes)
                let attHigh = NSAttributedString(string: cb.normalTitle, attributes: highAttributes)

                btn.setAttributedTitle(attNorm, for: .normal)
                btn.setAttributedTitle(attHigh, for: .highlighted)

                if cb.selectedTitle != "" {
                    let attSel = NSAttributedString(string: cb.selectedTitle, attributes: normAttributes)
                    btn.setAttributedTitle(attSel, for: .selected)
                }

                btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true

                btn.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)

                svRow.addArrangedSubview(btn)

                idx += 1
            }

            svRows.addArrangedSubview(svRow)

        }

        view.addSubview(inputLabel)
        view.addSubview(svRows)

        let g = view.safeAreaLayoutGuide

        let cLeading = svRows.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0)
        cLeading.priority = .defaultHigh

        NSLayoutConstraint.activate([

            inputLabel.topAnchor.constraint(greaterThanOrEqualTo: g.topAnchor, constant: 16.0),
            inputLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            inputLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),

            cLeading,

            svRows.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            svRows.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),

            svRows.topAnchor.constraint(equalTo: inputLabel.bottomAnchor, constant: 16.0),

        ])

        // just so we can see the frame of the inputLabel
        inputLabel.backgroundColor = .gray

    }

    @objc func btnTapped(_ sender: Any?) -> Void {

        guard let btn = sender as? UIButton, let t = btn.currentAttributedTitle?.string, let curText = inputLabel.text else {
            return
        }

        if t == "⌫" || t == "⌦" {
            btn.isSelected = !btn.isSelected
        } else {
            inputLabel.text = curText + t
        }

    }

}

Result:

enter image description here

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • thank you again! I tried your method and it works. However there is something strange. First, the refresh time changes to be rather slow when the user changes the buttons layout. I can see the titles blinking. Second, the deg/rad button seems not to work correctly, where I use deg as normal, rad as selected. Finally, I tried to use your method only on 4 buttons(add, sub, mul, div). It maybe a solution. Thank you again. – michaelma Mar 08 '20 at 02:37
  • @MichaelMa - you'll need to provide a lot more information for me to understand what you are doing that causes *"refresh time changes to be rather slow when the user changes the buttons layout. I can see the titles blinking."*. As I said in my answer, this was just an example... obviously (or maybe not), there'd be no reason to set the titles ***and then change them*** to attributed titles. Also, for *"deg as normal, rad as selected"* it seems logical you would set a different `.selected` attributed title. – DonMag Mar 08 '20 at 13:15
  • For example: when the user turn his iPhone from portrait mode to landscape mode, the buttons layout will be re-arrange and refresh displaying(just as the system Calculator). As using the attributed titles, the behavior of the refreshing become uncomfortable: the button's background refresh quickly as normal, but the attributed title appears with about 0.2 second delay comparing to its background. Not as the same time as it should be. – michaelma Mar 09 '20 at 00:21
  • @MichaelMa - I added more sample code which produces a 5x5 grid of buttons. I don't notice delay any issues with device rotation. – DonMag Mar 09 '20 at 14:03
  • Thank you for your kind help. My English is not very good and may not fully express my gratitude to you. I tried to use your code by creating a new project and paste you code into ViewController.swift, but it fails to run. I don't know much about using IB, I create this UI completely programmatically. I just checked and tested again, the result is still same. If you would like to give me your email address, I will send you a screen shot to see it. My email is machengzhao@qq.com – michaelma Mar 09 '20 at 14:56
  • @MichaelMa - I posted this example as a GitHub repo: https://github.com/DonMag/CalcButtons ... code only - the only Storyboard is `LaunchScreen` – DonMag Mar 09 '20 at 15:57
  • @MichaelMa - if you don't see the "refresh delay" with my example, but you're still seeing it in your app, I can only guess that your code is doing something else to cause it. Difficult to say. If you can put your project up as a GitHub repo or zip it up and use a file-sharing service, I'll take a look at it. – DonMag Mar 10 '20 at 11:57
  • I had edited & posted some code in your answer, but it disappears now. Have you see it? @DonMag – michaelma Mar 10 '20 at 16:06
  • @MichaelMa - I can see the code you put in as an "edit" ... My comment remains: Your code is doing the same thing, so if you are still seeing a "refresh / layout change delay" then it is almost certainly being caused by something else your code is doing. – DonMag Mar 10 '20 at 16:23
  • Dear @DonMag, I have used your method in my app Yes Calculator and have approved by Apple App Store. The blink problem still exists, but it's not obvious, so let it go. You can download my app to see the effect. It's free to download now. Thank you again. – michaelma Mar 29 '20 at 14:23
  • @MichaelMa - I took a look at your app, and, of course, I can see what you're talking about. Are you re-setting the button titles on size change (to change fontSize, I'm guessing)? Can you reproduce that in a simple, single-button example? Really, though, it doesn't look bad - almost looks like an intentional "visual effect" - and if you ultimately leave it the way it is I can't imagine anyone thinking it is "not working as intended." – DonMag Mar 30 '20 at 12:56
  • Dear @DonMag, yes, the font size is recalculated & refreshed everytime when the buttons are refreshed. – michaelma Mar 30 '20 at 14:05