Skip to content

A drag that starts on a SwiftUI button also presses it (iOS 16 and 17) #692

Description

@fawzirifai

Description

On iOS 16 and 17, if a drag of the panel starts on a SwiftUI Button in the content, the button stays highlighted for the whole drag and its action fires when the finger lifts. From iOS 18 it behaves as expected: the button's highlight clears as the drag begins and the action doesn't fire.

While the surface rests at an anchor, Core.gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) lets the panel's pan recognize alongside any recognizer that isn't a pan, swipe, rotation, screen-edge pan or pinch (the default: branch). Up to iOS 17, SwiftUI handles all of a hosting view's gestures through one tap-type recognizer, and running alongside the pan, nothing cancels it when the drag begins. A UIButton isn't affected, because the pan's touch cancellation (cancelsTouchesInView) reaches the button but not a gesture recognizer. From iOS 18, SwiftUI's per-touch recognizers let the pan cancel them, so the bug disappears there.

Expected behavior

As on a native sheet: once the drag begins, the pending tap fails. The button's highlight clears and its action doesn't run.

Actual behavior

The button stays highlighted during the drag, and its action runs on release. The same happens with a custom ButtonStyle and with .plain.

Steps to reproduce

  1. Run the view controller below on iOS 16 or 17.
  2. Put a finger on the button and drag the panel up or down a short way, so the finger stays on the button, then lift.
  3. The count goes up.

Code example that reproduces the issue

import UIKit
import SwiftUI
import FloatingPanel

final class DragOnButtonViewController: UIViewController {
    private let fpc = FloatingPanelController()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemGray5
        fpc.set(contentViewController: UIHostingController(rootView: ButtonSheet()))
        fpc.addPanel(toParent: self)
    }
}

struct ButtonSheet: View {
    @State private var presses = 0

    var body: some View {
        VStack {
            Button("Pressed \(presses) times") { presses += 1 }
                .buttonStyle(.borderedProminent)
                .controlSize(.large)
            Spacer()
        }
        .padding(.top, 32)
        .frame(maxWidth: .infinity)
    }
}

How do you display panel(s)?

  • Add as child view controllers

How many panels do you displays?

  • 1

Suggested fix

Don't recognize alongside those recognizers: return false from the default: branch. A tap that is still pending when the drag begins then fails, and a tap with no drag is unaffected, because the pan never begins.

default:
    return false

What I don't know is why the pan runs alongside them. It was return true ("Should always recognize tap/long press gestures in parallel"), and ec0e8cb narrowed it to "only while the surface is at an anchor", but neither commit gives a reason. If something relies on it (a long-press-then-drag in the content, say), a narrower rule might be needed instead, for example leaving out only tap recognizers.

In the meantime an app can override this through panGestureRecognizer.delegateProxy.

With this change, on iOS 17, the same drag no longer presses the button in any of those three styles, and a tap still does.

Happy to open a PR once we agree on the rule.

Environment

Library version

main (de51f81); also 3.2.2

Installation method

  • Source, built directly from Sources/ for the repro

iOS version(s)

iOS 17.0 simulator (iPhone SE, 3rd generation) and iOS 16 (iPhone 8): affected. iOS 18 and later: fine.

Xcode version

27.0

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions