[Pitch] Switchable setters (sugar)

Yesterday in a code review we saw someone overriding UIButton's func setTitle(String?, for: UIControl.State) and someone said, "Can't we do better? This API feels pretty Obj. C-y."

What if Swift could let the call site of such a function look like:

title.normal = "Normal Title"
title.selected = "Selected Title"
/// etc..

Thoughts?

1 Like

Would this require getter functions too? I find often times that the lack of getters sometimes faces this type of pattern. I presume that you are inferring that only a frozen enumeration would be allowed for this; which I would imagine that the authors of UIControl.State might want to have the option of one day adding an additional case if it so warranted it.

1 Like

I mean, it is an Obj-C API, inherited from UIKit. If you want those properties, you could write them now (though I'd name them <state>Title). Otherwise what are you proposing?

1 Like

You could use a subscript, which solves the getter issue and gives you access to mutating methods:

extension UIButton {
  subscript(title state: UIControl.State) -> String? {
    get { title(for: state) }
    set { setTitle(newValue, for: state) }
  }
}

let button = UIButton()
button[title: .normal] = "Normal"
button[title: .selected] = "Selected"
button[title: .normal]?.append(" Button")

It would be nice to have some kind of "parameterised computed properties" - basically something which looks like a function at the call site (with parameters), but works like a computed property with get/set pairs and _modify accessors.

button.title(.normal) = "Normal"
button.title(.selected) = "Selected"
button.title(.normal)?.append(" Button")
8 Likes

I use an extension that gives me a similar syntax to Karl's:

button.states[.normal].title = "Normal"
button.states[.selected].title = "Selected"
button.states[.normal].image = UIImage(named: "NormalButton")

Here's the extension:

extension UIButton {
    var states: StatesWrapper { .init(button: self) }

    struct StatesWrapper {
        let button: UIButton

        subscript(state: UIControl.State) -> StateWrapper {
            .init(button: button, state: state)
        }
    }

    struct StateWrapper {
        let button: UIButton
        let state: UIControl.State

        var title: String? {
            get { button.title(for: state) }
            nonmutating set { button.setTitle(newValue, for: state) }
        }

        var attributedTitle: NSAttributedString? {
            get { button.attributedTitle(for: state) }
            nonmutating set { button.setAttributedTitle(newValue, for: state) }
        }

        var titleColor: UIColor? {
            get { button.titleColor(for: state) }
            nonmutating set { button.setTitleColor(newValue, for: state) }
        }

        var titleShadowColor: UIColor? {
            get { button.titleShadowColor(for: state) }
            nonmutating set { button.setTitleShadowColor(newValue, for: state) }
        }

        var image: UIImage? {
            get { button.image(for: state) }
            nonmutating set { button.setImage(newValue, for: state) }
        }

        var backgroundImage: UIImage? {
            get { button.backgroundImage(for: state) }
            nonmutating set { button.setBackgroundImage(newValue, for: state) }
        }

        @available(iOS 13.0, macCatalyst 13.0, tvOS 13.0, *)
        var preferredSymbolConfiguration: UIImage.SymbolConfiguration? {
            get { button.preferredSymbolConfigurationForImage(in: state) }
            nonmutating set { button.setPreferredSymbolConfiguration(newValue, forImageIn: state) }
        }
    }
}
8 Likes

It’s utterly boring, but worth pointing out that a nested struct grants the OP’s syntax wish:

struct ButtonStateProperties<PropType> {
    var normal: PropType
    var selected: PropType?
    // ...etc...
}

struct Button {
    var title: ButtonStateProperties<String>
    var backgroundColor: ButtonStateProperties<Color>
    // ...etc...
}
someButton.title.normal = "Engage!"  // works

This has the advantage over using an enum that some of the state values can be optional (selected above) while normal is required.


For a little more language-evolution-y excitement, one could use property wrappers to make normal the default, and expose other states through the projected value:

struct ButtonStateProperties<PropType> {
    var normal: PropType
    var selected: PropType?
    // ...etc...
}

@propertyWrapper struct ButtonProperty<PropType> {
    var projectedValue: ButtonStateProperties<PropType>

    var wrappedValue: PropType {
        get { projectedValue.normal }
        set { projectedValue.normal = newValue }
    }

    init(wrappedValue normalValue: PropType) {
        projectedValue = ButtonStateProperties(normal: normalValue)
    }
}

struct Button {
    @ButtonProperty var title: String
    @ButtonProperty var backgroundColor: Color
    // ...etc...
}
someButton.title = "Engage!"  // Sets the normal title
someButton.$title.selected = "Engagissimo!"

I can also imagine somewhat more unreasonable things one could do with dynamic keypath member lookup. In any case, it seems to me the language doesn’t need new features to improve on the legacy design from UIKit.

3 Likes

I call this a "named subscript" because it has a history of being referred to as a "named indexer" in C#.

button.titles[.normal] = "🐈‍⬛"
button.titles[.normal] // "🐈‍⬛"
extension UIButton {
  var titles: ObjectSubscript<UIButton, UIControl.State, String?> {
    .init(
      self,
      get: UIButton.title,
      set: UIButton.setTitle
    )
  }
}

It's easiest to work with with classes, but still doable otherwise using UnsafeMutablePointer. It would be much better if it were a real language feature.

/// An emulation of the missing Swift feature of named subscripts.
/// - Note: Argument labels are not supported.
public struct ObjectSubscript<Object: AnyObject, Index, Value> {
  public typealias Get = (Object) -> (Index) -> Value
  public typealias Set = (Object) -> (Value, Index) -> Void

  public unowned var object: Object
  public var get: Get
  public var set: Set
}

public extension ObjectSubscript {
  init(
    _ object: Object,
    get: @escaping Get,
    set: @escaping Set
  ) {
    self.object = object
    self.get = get
    self.set = set
  }

  subscript(index: Index) -> Value {
    get { get(object)(index) }
    nonmutating set { set(object)(newValue, index) }
  }
}

Good call on the subscript!

(On phone in bed so not long enough explaining and best links, but wanted to chip in with some UIKit and UIButton setup code examples)

I wrote ViewComposer to rid of verbose non Swifty customizarion of UIKit views. I’ve not updated it since 2018, but might give you some ideas. Allows you to write:

let button: UIButton = [.color(.red), .text("Red"), .textColor(.blue)]

Supports different texts for different button states too.

You might also find some simpler UIKit styling syntax in Zhip, I customise a button here like so:

createNewWalletButton.withStyle(.primary) {
            $0.title(€.Button.newWallet)
        }

Using Extensions here

Remember that UIControl.State is an OptionSet and there are cases where a combination of states must be used. I don't really see this as Obj-C style. The requirement is a setter that takes two parameters, which is often awkward.

1 Like

All cases. The name of any OptionSet needs to be plural or end with the word Set in order to be accurate. It's UIControl.States in reality, even when it's a unit set.

This does not change that it's an Objective-C API that should be a subscript in Swift.

so many different ways to skin a cat.. this is what i use in UIKit branch:

var button = UIButton()

func someMethod() {
    VGroup { // inspired by swift ui
        button
            .title("title") // default "state" is .normal
            .title(.normal, "title") // "state" specified
            .title(localized(.title)) // localized
            .titleColor(#colorLiteral(red: 0.4, green: 0.01, blue: 0.19, alpha: 1))
            .titleColor([.highlighted, .selected], .red)
            .font(font)
            .image(#imageLiteral("photo"))
            .image(imageResource(.imageKey)) // "typed" / checked image
            .action { [weak self] in // no more obj-c selectors
                guard let self = self else { return }
                print("button pressed")
            }
    }
}