How to Create a UIButton Programmatically in Swift

How to Create a UIButton Programmatically in Swift

Written by

in

Quick answer: To create a UIButton programmatically in modern Swift, instantiate it using UIButton(configuration:primaryAction:). Set up a UIButton.Configuration (such as .filled() or .tinted()), pass a UIAction handler to respond to taps, add the button to the view hierarchy with view.addSubview(button), and position it using Auto Layout constraints.

Why this happens

When transitioning away from Storyboards and XIB files to code-only UIKit architectures, buttons must be manually instantiated, configured, positioned, and attached to the responder chain.

Several common stumbling blocks occur when doing this in Swift:

  1. Missing view hierarchy: Initializing a button without calling view.addSubview(button) leaves the view detached in memory, so it never renders on screen.
  2. Zero frame or missing constraints: A button created with UIButton() has a default frame of CGRect.zero. Without either setting a frame or activating Auto Layout constraints with translatesAutoresizingMaskIntoConstraints = false, the button has zero dimensions or renders off-screen.
  3. Selector mismatch: The older target-action pattern (#selector) requires the target function to be exposed to the Objective-C runtime via @objc. Forgetting this attribute causes a compiler error.

Modern iOS provides two distinct ways to build buttons in code: the modern UIButton.Configuration and UIAction API, and the classic addTarget(_:action:for:) selector approach.

How to fix it

1. Modern approach: UIButton.Configuration and UIAction

The standard way to build a button programmatically in modern iOS is using UIButton.Configuration alongside closure-based UIAction handlers. This eliminates the need for @objc selectors and provides built-in system styling.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupButton()
    }

    private func setupButton() {
        // 1. Define the visual configuration
        var config = UIButton.Configuration.filled()
        config.title = "Continue"
        config.subtitle = "Step 2 of 3"
        config.image = UIImage(systemName: "arrow.right")
        config.imagePlacement = .trailing
        config.imagePadding = 8
        config.baseBackgroundColor = .systemBlue
        config.baseForegroundColor = .white
        config.cornerStyle = .medium

        // 2. Attach a primary action closure
        let action = UIAction { [weak self] _ in
            self?.handleContinueTap()
        }

        // 3. Instantiate the button
        let button = UIButton(configuration: config, primaryAction: action)
        button.translatesAutoresizingMaskIntoConstraints = false

        // 4. Add to the view hierarchy
        view.addSubview(button)

        // 5. Constrain layout
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.heightAnchor.constraint(equalToConstant: 50),
            button.widthAnchor.constraint(greaterThanOrEqualToConstant: 200)
        ])
    }

    private func handleContinueTap() {
        print("Button was tapped!")
    }
}

Using [weak self] in the action closure prevents retain cycles if the button or view controller retains closures that reference self.

2. Classic approach: addTarget with #selector

If you maintain older codebases or prefer explicit method dispatch, use UIButton(type: .system) with addTarget(_:action:for:).

import UIKit

class ClassicViewController: UIViewController {

    private let actionButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Submit", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .systemIndigo
        button.layer.cornerRadius = 8
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground

        // Attach the target-action
        actionButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)

        view.addSubview(actionButton)

        NSLayoutConstraint.activate([
            actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            actionButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            actionButton.widthAnchor.constraint(equalToConstant: 160),
            actionButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }

    @objc private func buttonTapped(_ sender: UIButton) {
        print("Tapped button with title: \(sender.currentTitle ?? "")")
    }
}

Key rules for this pattern:
– The target function must use the @objc modifier so the Objective-C runtime can look up the selector.
– The event should typically be .touchUpInside, which fires when a user lifts their finger inside the button bounds.

3. Positioning with Auto Layout vs. hardcoded frames

Avoid setting explicit frames like UIButton(frame: CGRect(x: 100, y: 100, width: 200, height: 50)) unless calculating layout manually inside viewDidLayoutSubviews(). Hardcoded frames do not adapt to different iPhone screen sizes, Dynamic Type size increases, or orientation changes.

When using Auto Layout:

// Required: disables the automatic frame-to-constraint translation
button.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(button)

NSLayoutConstraint.activate([
    button.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
    button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
    button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
    button.heightAnchor.constraint(equalToConstant: 50)
])

Anchoring to view.safeAreaLayoutGuide ensures the button stays above the home indicator and does not collide with notches or the Dynamic Island.

How to prevent it

  • Always set translatesAutoresizingMaskIntoConstraints = false on your button immediately before or after setting constraints.
  • Add the button before activating constraints: Calling NSLayoutConstraint.activate between the button and its parent view will crash if the button has not already been added using addSubview(_:).
  • Use safe area layout guides: Pin buttons to view.safeAreaLayoutGuide rather than view edges to avoid home indicator overlapping.
  • Keep closures weak: Always use [weak self] inside UIAction closures to avoid memory leaks.

FAQ

Why does my button appear invisible when created in code?

A button created programmatically has no default size unless you provide an intrinsic content size through a title, configuration, or explicit Auto Layout constraints. If you omit both frame coordinates and layout constraints, the button defaults to (0, 0, 0, 0).

What is the difference between UIButton(type: .system) and UIButton()?

UIButton(type: .system) includes default UIKit styling, such as standard tint colors and the dimming highlight state when pressed. A plain UIButton() creates a .custom type button with no default highlight animations, requiring manual styling for pressed states.

Can I still use setTitle(_:for:) with UIButton.Configuration?

When using UIButton.Configuration, setting properties through legacy methods like setTitle(_:for:) or setImage(_:for:) can cause conflicting updates or be ignored entirely. Instead, update the configuration struct directly and assign it back to button.configuration.

Sources & credits

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