Daily coverage of WWDC21.
A Swift by Sundell spin-off.

Taking UIKit’s new button configuration API for a spin

Published at 09:10 GMT, 10 Jun 2021
Written by: John Sundell

SwiftUI isn’t the only UI framework that has received significant new APIs and updates during this year’s WWDC. In fact, one of UIKit’s most fundamental classes, UIButton, is getting a brand new system for configuring many aspects of a button’s appearance and behavior.

Let’s take it for a quick spin!

From imperative to descriptive

Although the default appearance of UIButton has changed quite a lot since its original introduction in the very first public release of the iOS SDK (or iPhone SDK as it was known as back then), many of its core configuration APIs have remained mostly unchanged during all of those years. For example, the way we configure the title, image, and background color of a button looks more or less the same today as it did back in 2008:

let likeButton = UIButton(type: .system)
likeButton.setTitle("Like", for: .normal)
likeButton.setImage(
    UIImage(systemName: "hand.thumbsup"),
    for: .normal
)
likeButton.tintColor = .white
likeButton.backgroundColor = .systemBlue
likeButton.layer.cornerRadius = 10
likeButton.contentEdgeInsets = UIEdgeInsets(
    top: 10, left: 8, bottom: 10, right: 8
)

Obviously, there were no SFSymbols back when the iPhone SDK was first released, and there was no Swift either for that matter, but you get my point.

That’s not a bad thing, though. Quite the opposite, really. The fact that many of UIKit’s core classes, including UIButton, have stood the test of time so well is really a great achievement for the engineers who originally designed those initial public APIs.

But, times have changed, and with the introduction of frameworks like SwiftUI many developers are now expecting to be able to work at a slightly more higher level, using more declarative and descriptive APIs. So, this year, Apple is introducing a really nice (and quite powerful) new system for configuring UIKit-based buttons, which enables us to easily describe configurations that can then be applied to any UIButton instance:

var config = UIButton.Configuration.filled()
config.title = "Like"
config.image = UIImage(systemName: "hand.thumbsup")
config.background.backgroundColor = .systemBlue
config.cornerStyle = .medium
config.imagePadding = 5
config.contentInsets = NSDirectionalEdgeInsets(
    top: 10, leading: 8, bottom: 10, trailing: 8
)

let likeButton = UIButton(type: .system)
likeButton.configuration = config

Note how we didn’t start by creating a brand new UIButton.Configuration value. Instead, we’re deriving our custom configuration from the built-in filled one. That’s one of my favorite aspects of this new API — since it’s value type-based, it’s incredibly composable. Not only does that let us base our configurations on system ones, we can also then continue to compose and combine those custom configurations as well, enabling us to build up a “hierarchy” of increasingly specific configurations.

For example, we might want to extract parts of the above likeButton configuration into a more general-purpose action configuration, and then implement our like variant as a specialization of that more generic configuration. That can now be easily done, without having to implement any complex UIButton subclasses, using a set of static factory methods:

extension UIButton.Configuration {
    static func action() -> Self {
        var config = UIButton.Configuration.filled()
        config.background.backgroundColor = .systemBlue
        config.cornerStyle = .medium
        config.imagePadding = 5
        config.contentInsets = NSDirectionalEdgeInsets(
            top: 10, leading: 8, bottom: 10, trailing: 8
        )
        return config
    }

    static func like() -> Self {
        var config = action()
        config.title = "Like"
        config.image = UIImage(systemName: "hand.thumbsup")
        return config
    }
}

With the above in place, we can now assign our like configuration to any UIButton instance, simply by doing this:

let likeButton = UIButton(type: .system)
likeButton.configuration = .like()

I wouldn’t go so far as to call this new configuration API “declarative”, as it still follows a more imperative style that’s a perfect match for the overall design of UIKit itself. Instead, I’d call it “descriptive”, as we’re now describing our button styles separately, rather than directly mutating UIButton properties directly — which creates a very neat decoupling between our concrete button instances and the styles and behaviors that are being applied to them.

Dynamic configurations

The new configuration API also supports completely dynamic configurations as well. While we could always simply access a button’s configuration property and modify it that way (since UIButton is a good old fashioned class), there’s a dedicated configurationUpdateHandler API that lets us apply any dynamic styles and behaviors that might change over time.

Here we’re using that API to dynamically assign the title and image of our likeButton depending on whether the user has liked a given video:

class VideoViewController: UIViewController {
    var videoIsLiked: Bool {
        // This tells UIKit that we'd like our button's configuration
        // update handler to be called as soon as possible:
        didSet { likeButton.setNeedsUpdateConfiguration() }
    }

    private lazy var likeButton = UIButton(type: .system)

    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        likeButton.configuration = .action()

        // It's important to use either a weak or unowned self
        // capture here, since otherwise we'll cause a retain
        // cycle (since self is the owner of likeButton):
        likeButton.configurationUpdateHandler = { [weak self] button in
            if self?.videoIsLiked == true {
                button.configuration?.title = "Remove like"
                button.configuration?.image = UIImage(
                    systemName: "x.circle"
                    )
            } else {
                button.configuration?.title = "Like"
                button.configuration?.image = UIImage(
                    systemName: "hand.thumbsup"
                )
            }
        }
        
        ...
    }
}

Encapsulating all dynamic styles within the above kind of configurationUpdateHandler closure seems to be a good practice, as that ensures that all of our button styles will be applied in a very predictable manner. Also, it makes it easy to see exactly what configurations and states that each button can be in, since all of that code is now implemented in a single location.

Many other styling options

The new button configuration API isn’t just a new way to assign properties that UIButton has been supporting for years — it also includes a ton of new features that are brand new. While I’m not able to go through every single aspect in this first look article, here are two of my favorite new additions:

First, we can now easily tell a button to display a loading spinner, which is incredibly convenient when building buttons that kick off some form of asynchronous task, such as a network call:

config.showsActivityIndicator = true

Second, UIButton now has proper support for subtitles, which are rendered using a smaller font below the button’s main title:

config.title = "Log in"
config.subtitle = "Access member-exclusive features"

There’s also additional support for displaying menus from a button, more layout options when it comes to margins and paddings, image positioning controls, and much more.

Conclusion

Who would’ve thought that we would call 2021 a “great year for UIButton”? I’m incredibly happy about this new button configuration API, as it takes many common and previously time-consuming layout and styling tasks and turns them into one-line configuration properties. I can’t wait to explore this new API further, and to start implementing it within my various projects (once I can drop support for iOS 14, that is).

To learn much more about this new configuration API, and all of the new capabilities that have been added to UIButton this year, make sure to check out the excellent WWDC session “Meet the UIKit button system”.

Thanks for reading!

Written by: John Sundell
RevenueCatRevenueCat

Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.