Buttons in Stacks in Forms

Is this some kind of Dr Seuss story? Probably not, ‘Circles in buttons in loops in stacks in scroll views in forms’ doesn't really rhyme.

The initial approach

This problem was noticed in SwiftUI under the Xcode 11 beta 3 and beta 4 releases: it may be fixed since then, or it may be intentional behavior, or we may be doing something stupid.

Draw a Circle, wrap it up in a Button, wrap that up in a ForEach to get multiple buttons, put them in all in an HStack to get them all lined up in a row, wrap that row in a ScrollView so that we can scroll to reach the ones offscreen, and everything should work nicely! And it did.

struct ContentView: View {
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(things, id: \.name) { thing in
                    Button(action: {
                        print("thing = \(thing.name)")
                    }) {
                        Circle()
                            .frame(width: 80.0, height: 80.0)
                            .foregroundColor(thing.color)
                    }
                }
            }
        }
    }
}

But...

But put that whole thing inside a Form. At first everything looks good—the HStack now appears at the top of the screen (which is nice, plus we also get vertical scrolling when the contents are large enought to exceed the screen height); we can see the white row against the gray background of the form; and the row still scrolls as before.

Just one slight problem—the buttons failed to respond. Tapping on them did nothing, random (and frustrated) tapping on the row would sometimes trigger an action on all the buttons simutaneously.

Enter .tapAction{}

One solution was to move away from using the Button() and use .tapAction instead, a technique shown in WWDC 2019 "Introducing SwiftUI: Building Your First App"

struct ContentView: View {
    var body: some View {
        Form {
            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    ForEach(things, id: \.name) { thing in
                        Circle()
                            .frame(width: 80.0, height: 80.0)
                            .foregroundColor(thing.color)
                            .tapAction {
                                print("thing = \(thing.name)")
                        }
                    }
                }
            }
        }
    }
}

This worked—tapping each Circle does the right thing—but with textual buttons the normal color changes on tapping are missing, so there are still a few things to fix. Although—as is so often the case—once we got the buttons working inside a form we decided that the form appearance wasn't appropriate in this instance, so took everything back out.

On a purely subjective note, the "this is a circle and when tapped this is what it does" syntax seems nicer than the "this is a button that does this and this is what it looks like" syntax. Perhaps instead of Button( action: { action }) { content } we could just have .buttonAction{ action } instead?

Discuss?

Haven't added a commenting system to this app, and probably won't. So if you have any feedback—positive or negative—either say it via email to david@cocoacheerleaders.com or on Twitter at @CocoaCheer.