Fix: UIButton titleLabel Font Not Updating in Swift

Fix: UIButton titleLabel Font Not Updating in Swift

Written by

in

Quick answer: In iOS 15 and later, buttons created with modern styles (Plain, Gray, Tinted, Filled) use UIButton.Configuration, which overrides button.titleLabel?.font. To change the font, assign an AttributedString with the desired UIFont or Font container to button.configuration?.attributedTitle, or switch the button style to Default (Legacy) in Storyboard if you prefer using titleLabel?.font.

Why this happens

Setting a font on a UIButton typically fails or gets ignored for two distinct reasons in modern iOS development. First, the old property myButton.font was deprecated in iOS 3.0 and subsequently removed from modern Swift SDK interfaces. Calling .font directly on a UIButton instance causes compiler errors such as Value of type 'UIButton' has no member 'font'.

Second, developers who switch to button.titleLabel?.font = UIFont.systemFont(...) often find that their font changes are completely ignored at runtime. This happens because iOS 15 introduced UIButton.Configuration. If your button uses one of the modern configuration styles—such as Plain, Tinted, Gray, or Filled—UIKit manages label properties internally through its configuration pipeline. Every time the control redraws or updates state, UIKit wipes out direct mutations to titleLabel and reapplies the attributes declared in UIButton.Configuration.

If you created the button in Interface Builder (Xcode Storyboards or XIB files), Xcode defaults new buttons to the Plain style rather than the legacy Default style. As long as a button uses modern configuration styles, direct assignments to titleLabel.font or titleLabel.textColor will not appear on screen.

How to fix it

1. Use UIButton.Configuration with AttributeContainer (iOS 15+)

For any modern UIKit project running on iOS 15 or later, use UIButton.Configuration to set fonts. Build an AttributeContainer containing your custom or system font, then apply it to the configuration’s attributedTitle.

import UIKit

func configureModernButton(button: UIButton) {
    var config = button.configuration ?? UIButton.Configuration.filled()

    // Create the typography attributes
    var container = AttributeContainer()
    container.font = UIFont.systemFont(ofSize: 18, weight: .semibold)

    // Apply title text and typography together
    config.attributedTitle = AttributedString("Submit Order", attributes: container)

    // Assign back to the button
    button.configuration = config
}

If you need dynamic font scaling that responds to Dynamic Type, use UIFontMetrics:

var container = AttributeContainer()
let baseFont = UIFont.systemFont(ofSize: 17, weight: .bold)
container.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: baseFont)

config.attributedTitle = AttributedString("Continue", attributes: container)
button.configuration = config

2. Update Font via configurationUpdateHandler

When your button changes state (such as highlighted, disabled, or selected) and needs to preserve or alter typography dynamically, configure the font inside configurationUpdateHandler:

import UIKit

button.configuration = UIButton.Configuration.plain()
button.configurationUpdateHandler = { button in
    var config = button.configuration

    var container = AttributeContainer()
    let fontSize: CGFloat = button.isHighlighted ? 15 : 16
    container.font = UIFont.systemFont(ofSize: fontSize, weight: .medium)

    config?.attributedTitle = AttributedString("Save Changes", attributes: container)
    button.configuration = config
}

This ensures that UIKit does not reset your font during state transitions.

3. Use titleLabel.font with Legacy Buttons

If your button does not use UIButton.Configuration, or if you explicitly want to use the legacy UIKit button hierarchy, ensure the button is initialized without a configuration.

In code:

import UIKit

// Create a button without a configuration
let legacyButton = UIButton(type: .system)
legacyButton.setTitle("Tap Here", for: .normal)

// Direct access works on non-configured buttons
legacyButton.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)

In Storyboards or XIBs:
1. Open your storyboard and select the target UIButton.
2. Open the Attributes Inspector (⌥ ⌘ 5).
3. Look at the Style dropdown at the top. Change it from Plain, Gray, Tinted, or Filled back to Default.
4. Set the Font field in Interface Builder, or assign button.titleLabel?.font in your view controller’s viewDidLoad().

4. Apply Custom Fonts by PostScript Name

When using custom bundled .otf or .ttf fonts, construct the UIFont using the exact PostScript name, then pass it to the AttributeContainer:

import UIKit

func applyCustomFont(to button: UIButton) {
    guard let customFont = UIFont(name: "AvenirNext-DemiBold", size: 18) else {
        return
    }

    var config = button.configuration ?? UIButton.Configuration.plain()
    var container = AttributeContainer()
    container.font = customFont

    config.attributedTitle = AttributedString("Explore", attributes: container)
    button.configuration = config
}

Make sure the custom font file is listed under the UIAppFonts key (Fonts provided by application) in your Info.plist and included in your app target’s Copy Bundle Resources build phase.

How to prevent it

  • Avoid accessing button.titleLabel?.font on buttons that have non-nil button.configuration instances.
  • Check the Style property in the Interface Builder Attributes Inspector whenever a programmatic font does not appear; change it to Default if you are targeting legacy code.
  • Standardize on UIButton.Configuration and AttributedString for all new UIKit code targeting iOS 15 and later.
  • Do not use button.font, which was removed in early iOS SDK revisions.
  • Always set button titles and typography together via config.attributedTitle or button.setTitle(_:for:) rather than mutating button.titleLabel?.text directly.

FAQ

Why does setting button.titleLabel?.text also fail?

In UIKit, titleLabel is managed directly by the button instance. Setting button.titleLabel?.text = "..." will be overwritten whenever the button state updates; you should always use button.setTitle("Text", for: .normal) for legacy buttons or configuration.title / configuration.attributedTitle for modern buttons.

How do I check if my button uses UIButton.Configuration in code?

Inspect button.configuration. If button.configuration is not nil, the button uses the modern configuration pipeline, and any direct adjustments made to button.titleLabel?.font will be overwritten by UIKit.

Can I use SwiftUI Font with UIButton.Configuration?

No, UIKit’s AttributeContainer.font expects a UIFont instance, not a SwiftUI Font. Use UIFont.systemFont(ofSize:weight:) or UIFontMetrics to define your typography.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.