Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why buttons don't work when embedded in a SwiftUI Form?

Tags:

I'm building a SwitUI form. I need to draw some custom sliders. For that, I made a view (SliderView) that I include in the form several times. It took me some time to manage the Binding to get in the parent view the value of the Sliders, but I was able to do it :-) The big mystery I'm not able to understand is why the buttons I placed in the child view, don't work when they are embedded in a Form. They work fine in a VStack. Here the code:

//  SliderView.swift  import SwiftUI  struct SliderView: View {     var name: String     var min: Double     var max: Double     var step: Double     var defaultValue: Double      let numberFormatter: NumberFormatter = {         let formatter = NumberFormatter()         formatter.minimumFractionDigits = 0         formatter.maximumFractionDigits = 2         formatter.minimumIntegerDigits = 1         return formatter     }()       @Binding var selectedValue: Double     var body: some View {         HStack{             Text("\(name)")             Slider(value: $selectedValue, in: self.min...self.max, step: self.step)             Button(action: {                 if (self.selectedValue - self.step) >= self.min{                     self.selectedValue = self.selectedValue - self.step                 }             }) {                 Image(systemName:"minus.circle")             }             Text("\(numberFormatter.string(from: NSNumber(value: selectedValue)) ?? "")")                 .padding(5)                 .onAppear {                     self.selectedValue = self.defaultValue                 }             Button(action: {                 if (self.selectedValue + self.step) <= self.max{                     self.selectedValue = self.selectedValue + self.step                 }             }) {                 Image(systemName:"plus.circle")             }           }      } }  struct SliderView_Previews: PreviewProvider {     @State static var myValue: Double = 1     static var previews: some View {         SliderView(name: "CPU", min: 1, max: 10, step: 0.5, defaultValue: 1, selectedValue: $myValue)     } } 

Now, part of the form that I'm building. The code compiles, the sliders work and the value of each of them can be seen in the parent view. However, the buttons to update the sliders step by step don't work (because they are embedded in the Form)

// //  ContentView.swift  import SwiftUI  struct ContentView: View {     @State var selectedCPU: Double = 1     @State var selectedRAM: Double = 1     @State var selectedDisk: Double = 50     var body: some View {          NavigationView{             Form{                 Section{                     SliderView(name: "CPU", min: 1, max: 16, step: 1, defaultValue: selectedCPU, selectedValue: $selectedCPU)                     SliderView(name: "RAM", min: 0.5, max: 128, step: 0.5, defaultValue: selectedRAM, selectedValue: $selectedRAM)                     SliderView(name: "SSD", min: 20, max: 500, step: 10, defaultValue: selectedDisk, selectedValue: $selectedDisk)                 }                 Section{                     Text("CPU: \(selectedCPU)")                     Text("RAM: \(selectedRAM)")                     Text("SSD: \(selectedDisk)")                 }             }         }       } }  struct ContentView_Previews: PreviewProvider {     static var previews: some View {         ContentView()     } } 

Just replacing the "Form" with a "VStack" in the upper code, the buttons just work fine. Anyone can shed some light? I think is something regarding the different layers in the Form. When I try to click on the buttons the whole row with the slider seems to be clicked, but nothing happens. I'm using Xcode 11 GM in MacOS 10.15 beta (19A558d) Thanks in advance!

like image 252
jarnaez Avatar asked Sep 15 '19 19:09

jarnaez


People also ask

How do I make a text button in SwiftUI?

SwiftUI's button is similar to UIButton , except it's more flexible in terms of what content it shows and it uses a closure for its action rather than the old target/action system. To create a button with a string title you would start with code like this: Button("Button title") { print("Button tapped!") }

Why does SwiftUI use some view?

First, using some View is important for performance: SwiftUI needs to be able to look at the views we are showing and understand how they change, so it can correctly update the user interface.

What is a button SwiftUI?

A Button is a type of control that performs an action when it is triggered. In SwiftUI, a Button typically requires a title text which is the text description of your button, and an action function that will handle an event action when triggered by the user.


1 Answers

From what I've been able to tell, Form rows behave differently when they contain a View that accepts a tap gesture, such as a button. When the row contains a Button, then when the user taps on that button, everything else in the same row also receives a tap gesture. So in your example above, it's not that your buttons are not working, what is actually happening is that they are both receiving a tap gesture and executing their action, but because of the logic in your code effectively cancels the effect of the other, it appears as if they are not working.

To help demonstrate this point, here is some test code I wrote:

    @State var count1 = 0     @State var count2 = 0      func buttonTest() -> some View {         HStack {             Button(action: {                 self.count1 += 1             }) {                 Text("Button 1: \(count1)")             }             Spacer()             Button(action: {                 self.count2 += 1             }) {                 Text("Button 2: \(count2)")             }         }     }      var body: some View {         VStack {             buttonTest()             Form {                 Section(header: Text("Test")) {                     buttonTest()                 }             }         }     } 

If you tap either button above the form, they work independently as expected, incrementing only their own count, but if you tap either button in the Form, or anywhere else in the row, then both buttons' actions execute and both counts increment.

I'm not sure why Apple do this, but I too am struggling to find a solution to this problem.

EDIT

Thanks to Shauket Sheikh, there is a solution, but it makes code re-use less elegant.

So when the Button is in a Form, you need to put the code into an .onTapGesture handler, but if the Button is outside a Form, you need the code in the action: handler.

I really hope this isn't Apple's long-term position on Forms: it's goes against the write once, use everywhere philosophy.

like image 195
pbc Avatar answered Sep 22 '22 00:09

pbc