Skip to content

Panel holds still while dragged over a horizontally scrolling row #693

Description

@fawzirifai

Description

When a drag of the panel starts on a horizontal scroll view in the content (a row of chips, say), the panel doesn't follow the finger: it holds still for the whole drag if the drag is slow. Related to #76, but that issue was about the two gestures colliding; here the panel waits for the row.

Core.gestureRecognizer(_:shouldRequireFailureOf:) makes the panel's pan wait for any other pan to fail ("Do not begin the pan gesture until these gestures fail"). A scroll view that can't scroll along the panel's axis doesn't fail on a drag along that axis until the touch ends, so the panel waits the whole time.

Expected behavior

A vertical drag that starts on the row moves the panel, and a horizontal one scrolls the row, as with two nested scroll views running in different directions.

Actual behavior

A slow vertical drag that starts on the row leaves the panel where it is, during the drag and after the finger lifts. Recorded at 10 fps: a 300pt drag at 100pt/s from a chip left the surface at the same position in every frame, while the same drag started on an empty part of the panel moved it 10pt per frame with the finger.

Steps to reproduce

  1. Run the view controller below.
  2. Put a finger on the row of chips and drag slowly upward.
  3. The panel doesn't move.

Code example that reproduces the issue

import UIKit
import FloatingPanel

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

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

/// A sheet whose content is one horizontally scrolling row of chips, like a list of categories.
final class RowViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let chips = UIStackView(arrangedSubviews: (1...12).map { number in
            var configuration = UIButton.Configuration.gray()
            configuration.title = "Category \(number)"
            configuration.cornerStyle = .capsule
            return UIButton(configuration: configuration)
        })
        chips.spacing = 8
        chips.translatesAutoresizingMaskIntoConstraints = false

        let row = UIScrollView()
        row.showsHorizontalScrollIndicator = false
        row.translatesAutoresizingMaskIntoConstraints = false
        row.addSubview(chips)
        view.addSubview(row)
        NSLayoutConstraint.activate([
            row.topAnchor.constraint(equalTo: view.topAnchor, constant: 32),
            row.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            row.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            row.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 44),
            chips.topAnchor.constraint(equalTo: row.contentLayoutGuide.topAnchor),
            chips.bottomAnchor.constraint(equalTo: row.contentLayoutGuide.bottomAnchor),
            chips.leadingAnchor.constraint(equalTo: row.contentLayoutGuide.leadingAnchor, constant: 16),
            chips.trailingAnchor.constraint(equalTo: row.contentLayoutGuide.trailingAnchor, constant: -16),
            chips.heightAnchor.constraint(equalTo: row.frameLayoutGuide.heightAnchor)
        ])
    }
}

How do you display panel(s)?

  • Add as child view controllers

How many panels do you displays?

  • 1

Suggested fix

Two parts, both in Core:

  1. Don't wait for a scroll view that can't scroll along the panel's axis. In shouldRequireFailureOf, before the final return true for pans:

    if let otherScrollView = otherGestureRecognizer.view as? UIScrollView, !scrollsAlongPanelAxis(otherScrollView) {
        return false
    }

    scrollsAlongPanelAxis(_:) is the content-size / alwaysBounce… check that shouldScrollingContentInMoving(from:to:) already makes per position, pulled out so both can use it.

  2. Over such a scroll view, begin only on a drag along the panel's axis. Once the panel stops waiting, it would otherwise take sideways drags from the row too. Add gestureRecognizerShouldBegin(_:) to Core: when the touch started inside a scroll view that doesn't scroll along the panel's axis, begin only if the translation is mostly along that axis. That's what the outer of two nested scroll views does.

With both parts applied, the same drag from a chip moves the panel with the finger, frame for frame as the control does, and a sideways drag on the row still scrolls the row without moving the panel. We've shipped this locally. Happy to open a PR with it if you'd like it upstream.

Environment

Library version

main (de51f81); also 3.2.2

Installation method

  • Source, built directly from Sources/ for the repro

iOS version(s)

iOS 27.0 simulator (iPhone 18 Pro)

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