1

I have a SwiftUI app with a complex dashboard view consisting of several other nested custom SwiftUI views containing SwiftUI buttons.

I want to prevent that these different buttons can be pressed simultaneously with multiple fingers and execute their actions at the same time, which is currently possible.

I could not find any solution yet which is working for me. E.g. in this SO question (How to prevent two buttons being tapped at the same time in swiftUI?) there are two solutions:

  1. Disable multitouch and enable exclusiveTouch in the init of my dashboard view:

     UIButton.appearance().isMultipleTouchEnabled = false
     UIButton.appearance().isExclusiveTouch = true
    
     UIView.appearance().isMultipleTouchEnabled = false
     UIView.appearance().isExclusiveTouch = true
    

This is absolutely not working for me, neither in the init for the view, nor when I put it globally in the @main init or other global entry points of the app.

  1. The other approach is to have a isEnabled state and toggle it on every button press and set all other buttons to disabled. But this solution will not work for me, because the actual SwiftUI buttons are partly nested in multiple layers in other views.

I could not understand, that this is such a big pain in SwiftUI. Somehow it should be possible to prevent to press multiple buttons at the same time.

Any other solutions?

mahega
  • 3,231
  • 4
  • 20
  • 32

1 Answers1

0

It's easy if you exploit an EnvironmentObject to manage all of the buttons in your dashboard. Let's implement the solution step by step.

  1. Create an enum to define the IDs of your buttons, for example:
enum ButtonId {
    case login
    case signup
    case forgotPassword
}
  1. Create a manager to keep the pressed button ID:
final class ButtonsManager: ObservableObject {
    @Published var pressedButtonId: ButtonId?
}
  1. Create a ButtonStyle capable of communicate with the buttons manager. This struct will tell the manager when the button is pressed/released. In order for you to check the difference between a pressed/released/enabled/disabled button I used different colors, but it's just an example, you can customize the style as you want.
struct ButtonsManagerStyle: ButtonStyle {
    @Environment(\.isEnabled) private var isEnabled: Bool
    @EnvironmentObject private var buttonsManager: ButtonsManager
    let buttonId: ButtonId

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .foregroundColor(.white)
            .contentShape(Rectangle())
            .background(isEnabled ? Color.red.opacity(configuration.isPressed ? 0.3 : 1) : Color.gray.opacity(0.8))
            .onChange(of: configuration.isPressed) { isPressed in
                if isPressed {
                    buttonsManager.pressedButtonId = buttonId
                } else {
                    buttonsManager.pressedButtonId = nil
                }
            }
    }
}
  1. Create a ViewModifier to apply to your buttons. It will be useful in order not to replicate some logic for all of your buttons in the dashboard:
struct ButtonsManagerModifier: ViewModifier {
    @EnvironmentObject private var buttonsManager: ButtonsManager
    let buttonId: ButtonId

    func body(content: Content) -> some View {
        content
            .buttonStyle(ButtonsManagerStyle(buttonId: buttonId))
            .disabled(buttonsManager.pressedButtonId != nil && buttonsManager.pressedButtonId! != buttonId)
    }
}
  1. Finally, let's use what we've implemented so far:
// MARK: - Root View

struct MyRootView: View {
    @StateObject private var buttonsManager = ButtonsManager()
    var body: some View {
        VStack(spacing: 50) {
            Button {
                print("Login did tap")
            } label: {
                Text("Login")
            }
            .modifier(ButtonsManagerModifier(buttonId: .login))

            SignupNestedView()

            ForgotPasswordNestedView()
        }
        .environmentObject(buttonsManager)
    }
}

// MARK: - Signup

struct SignupNestedView: View {
    var body: some View {
        Button {
            print("Signup did tap")
        } label: {
            Text("Sigup")
        }
        .modifier(ButtonsManagerModifier(buttonId: .signup))
    }
}

// MARK: - Forgot Password

struct ForgotPasswordNestedView: View {
    var body: some View {
        ForgotPasswordNestedNestedView()
    }
}

struct ForgotPasswordNestedNestedView: View {
    var body: some View {
        Button {
            print("Forgot password did tap")
        } label: {
            Text("Forgot Password")
        }
        .modifier(ButtonsManagerModifier(buttonId: .forgotPassword))
    }
}

All you have to do is injecting the environment object in your root (whatever it is) and remember to set the modifier we've created to your buttons in the dashboard. Here's the result:

enter image description here

superpuccio
  • 11,674
  • 8
  • 65
  • 93
  • Nice approach, will definitely try it. But still was hoping for a solution, where I have to add code to every existing button. Also I have the problem, that some buttons are part of a view, which is created by stuff coming from a network call. Then it will be difficult with your enum ID solution. – mahega Aug 30 '23 at 11:41
  • Consider that the code you have to add to your buttons it's just a single line of code. Also, consider that with a little more effort you can just create a view which is a button (let's say `MyButton`) which encapsulate the logic I described you. In this case you could just use `MyButton` instead of `Button` and you'll have the desired behaviour. – superpuccio Aug 30 '23 at 11:45
  • "Also I have the problem, that some buttons are part of a view, which is created by stuff coming from a network call. Then it will be difficult with your enum ID solution." I used an enum for simplicity, but you can use whatever you want if your buttons are not known at compile time. A string (the title of the button), an ID coming from the server, whatever. – superpuccio Aug 30 '23 at 11:47