Make UIControl More Swifty

In Objective-C, it is possible to set a property either by using dot notation (eg. foo.prop = bar) or by calling a setter (eg. [foo setProp: bar]).

In Swift, there’s only one way - the dot notation. If you needed custom behavior in Objective-C for getters or setters, you’d implement a setter and/or getter method. In Swift, you’d use computed properties like this:

var foo: Bar {
    get {
        [...]
    }
    set {
        [...]
    }
}

Because of this, using methods such as UIButton.setTitle(_:for:) in Swift bothers me. They feel out of place, since in Swift getters and setters are not methods.

Making them computed properties is not possible, since there’s no way to pass any arguments, and in this case an argument is necessary - UIControl.State.

It turns out that I’m not the only one who thinks that this is a problem. There’s a relatively popular library with native Swift extensions called SwifterSwift. And if you look at UIButtonExtensions.swift there, you’ll see that it’s almost entirely dedicated to solving this issue.

Here’s an example from there:

public extension UIButton {
    [...]
    @IBInspectable public var titleForNormal: String? {
        get {
            return title(for: .normal)
        }
        set {
            setTitle(newValue, for: .normal)
        }
    }
    [...]
}

There are a couple of problems here:

So, can we do better? I believe we can. Since these feel like they should be properties in Swift, but they need parameters, we have a perfect tool in our toolbox. I’m talking about subscripts. Instead of calling button.setTitle("Title", for: .normal) or setting a button.titleForNormal, how about setting button.titles[.normal]?

How can we implement this? Your first instinct might be something like this

extension UIButton {
    var titles: [UIControl.State: String] {
        get {
            [...]
        }
        set {
            [...]
        }
    }
}

That was my first instinct at least. But as we try to implement that, we run into problems.

The first thing that doesn’t allow us to do this, even before compiling, is UIControl.Stateis not hashable. Even though it’s simple to make it hashable, maybe there’s a reason it isn’t already… Fighting the system at this level just feels wrong.

Setting the values is simple enough to add, but - what if we want to set some value to nil. This is allowed, but impossible to implement unless we enumerate all possible values of UIControl.State. We already saw that enumerating it would be impractical. There would also be performance issues, since we would need to go through every permutation of values.

So, dictionary will not work. Is there something else we can do? Well - yes, there is. I wouldn’t be writing this if there wasn’t 😁

The answer lies in subscripts. It’s possible to make a custom one. The first thing we need is a custom type. Let’s call it UIControlStateValue and make it a class. We should also make it generic, since there are multiple types that can be set depending on state (eg. titleColor, title, image etc.). UIControlStateValue also needs to know how to get and set the value. For that we can use closures.

Here’s the complete class:

class UIControlStateValue<T> {
    private let getter: (UIControl.State) -> T?
    private let setter: (T?, UIControl.State) -> Void

    // The initializer is fileprivate here because all 
    // extensions are in a single file. If it's split 
    // in multiple files, this should be internal
    fileprivate init(getter: @escaping (UIControl.State) -> T?,
                     setter: @escaping (T?, UIControl.State) -> Void) {
        self.getter = getter
        self.setter = setter
    }

    subscript(state: UIControl.State) -> T? {
        get {
            return self.getter(state)
        } set {
            self.setter(newValue, state)
        }
    }
}

And here’s example usage in UIButton:

extension UIButton {
    var titles: UIControlStateValue<String> {
        return UIControlStateValue<String>.init(getter: self.title(for:), setter: self.setTitle(_:for:))
    }

    var titleColors: UIControlStateValue<UIColor> {
        return UIControlStateValue<UIColor>(getter: self.titleColor(for:), setter: self.setTitleColor(_:for:))
    }
}

So using this, we can set title using button.titles[.normal] = "Whatever". Assigning value to multiple states is also supported: button.titles[[.disabled, .focused]] = "Whatever" Getting the values is simply button.titles[.normal] or button.titles[[.disabled, .focused]]

The code can also support custom States, and if iOS adds more, it will support those too. Also, more importantly, this feels far more swifty™. At least it does to me, and I hope it does to some of you.