Exploring the New UISegmentedControl Initializer Available in iOS 14

The first beta SDK for iOS 14 was released on Monday, 22 June 2020 alongside the first developer beta build of the OS, bringing with it numerous new APIs and enhancements. One such enhancement is the new UIActions-based init(frame:actions:) initializer.

Before we take a look at how the new initializer works, let's go through the process of setting up a UISegmentedControl in iOS 13. A segmented control can be initialized with an array of strings that will become the segment titles in the UI.

let control = UISegmentedControl(items: ["Red", "Green", "Blue"])

Next we need to add a target to the segmented control with a selector to be executed when the control's primary action is triggered.

control.addTarget(
    self,
    action: #selector(didTapSegmentedControl),
    for: .primaryActionTriggered
)

Finally, we need to define the @objc-marked function that we passed to the selector above.

@objc private func didTapSegmentedControl(_ sender: UISegmentedControl) {
    let textColor: UIColor
    switch sender.selectedSegmentIndex {
        case 0: textColor = .systemRed
        case 1: textColor = .systemGreen
        case 2: textColor = .systemBlue
        default: textColor = .label
    }
    label.textColor = textColor
}

To the developer who has been writing UIKit code since they started development, this seems routine. However, this three step dance provides a large surface area for bugs, e.g. the manually configured binding between the control titles and the switching over the selected segment index. I could change the order of the strings in the segmented control initializer and forget to update the switch case in the selector function and this will lead to inconsistency in the UI.

The new initializer in iOS 14 SDK is based on the UIAction type introduced last year in iOS 13. An UIAction has an initializer that takes a title string and a closure to be run when the action is invoked. Here is a sample action:

let action = UIAction(title: "Title") { (action: UIAction) -> Void in
    <#Code#>
}

UISegmentedControl's new initializer takes an array of such actions and generates segments with the given title and executes the actions' closures when their corresponding segments are selected.

enum Color: String, CaseIterable {

    case red = "Red"
    case green = "Green"
    case blue = "Blue"

    var uiColor: UIColor {
        switch self {
            case .red: return .systemRed
            case .green: return .systemGreen
            case .blue: return .systemBlue
        }
    }

}

let segmentedControl = UISegmentedControl(
    frame: <#CGRect#>,
    actions: Color.allCases.map { [unowned label] color in
        UIAction(title: color.rawValue) { _ in
            label.textColor = color.uiColor
        }
    }
)

This new initializer configures everything about the segmented control in one place, the number of segments, their titles, and the code to run when a segment is tapped, making your code more organised and readable. In addition to this initializer, UISegmentedControl has new APIs to access an action at a segment , set an action at a segment, and insert an action at a segment.

In conclusion, this move away from target-actions to closure-based UIActions is one more step in the direction of the Swiftification of UIKit. A UIAction-based initializer are also available for UIControl and all of its subclasses, including your custom subclasses not predefined by UIKit.

A demo app with both the old target-action as well as the new closure-based UIAction implementations is available on GitHub.