Embracing the AppKit Ways

Earlier today, I was posting some ad-hoc thoughts about how I prefer SwiftUI layout over programmatic AppKit on Mastodon. I expected this to become a blog post later today.

But instead, I find myself thinking along the lines of: “am I holding this wrong?”

I’m programmatically setting up a list of radio buttons. A radio button group is only loosely associated – they need to share the target and action, and that’s that.

To identify radio button is which, you may use their tag. That helps to figure out which one was selected when the action is triggered.

An alternative way to identify which radio button in the group was selected, you can also === compare for object identity of the action sender. This requires storing each radio button as a property, though.

I was working without storing radio buttons for a couple of days. Their tag was (and is!) sufficient to handle the action properly. Also, I don’t enjoy the imperative setup of NSButton to become a radio button, so I abstracted that away – and now I don’t want to use this differently to capture the reference in a property instead.

How, then, do you toggle a radio button of a group programmatically if you don’t have a direct reference?

Turning a specific radio button .on doesn’t suffice – you also need to turn all the others .off. For me, that meant the radio button group would need to be available as an array anyway, so that I could iterate over the group easily and toggle the one with the appropriate tag. Meh.

Double-checking the Apple Forums if I was maybe overlooking something, I noticed that people iterated the subviews of a group container. Nobody questioned that approach.

Why was I not doing this?

What would that look like?

let selectedTag: Int = ...
let buttonsTargetingThisAction = self.subviews
    .compactMap { $0 as? NSButton } }
    .filter { $0.action == #function } // ❶
for button in buttonsTargetingThisAction {
    button.isChecked = (button.tag == selectedTag) // ❷

It’s not that bad, actually.

Note that ❶ I’m using #function to represent “this action” here. That only works if this code is part of the @objc action the button is targeting. And ❷ isChecked isn’t built-in but is my own NSButton extension.

After complaining about how I prefer to now work with SwiftUI where I can, I also have to take one step back and realize that I need to embrace AppKit and its Objective-C API heritage properly whenever I want to use it. There’s no point in making my life hard by programmatically creating the AppKit UI, then complain that it’s harder.

I could’ve used Xibs instead, which doesn’t sound that bad to me anymore, and @IBActions to quickly store references to all radio buttons per group. Since I didn’t, I should lean into traditional ways to work with AppKit views.