You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS中仿Apple Maps底部视图滑动动画的手势处理问题求助

Fixing Apple Maps-Style Bottom Sheet Pan Gesture in iOS

Hey there! Let's tackle this bottom sheet gesture issue you're having—Apple Maps-style bottom sheets are tricky but super satisfying when they work right. Since you already have your panVC added as a subview from the bottom, let's focus on fixing the pan gesture handling with practical, actionable steps.

Key Steps to Implement Smooth Pan Gesture Handling

1. Add the Pan Gesture Recognizer Correctly

First, make sure you've attached a UIPanGestureRecognizer to your panVC's main view, and handle any potential gesture conflicts upfront:

override func viewDidLoad() {
    super.viewDidLoad()
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
    // Allow simultaneous recognition if you have scroll views inside the sheet
    panGesture.delegate = self
    view.addGestureRecognizer(panGesture)
}

// Optional: Handle gesture conflicts with internal scroll views
extension YourPanVC: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        // Only allow simultaneous recognition if the scroll view is at the top
        guard let scrollView = otherGestureRecognizer.view as? UIScrollView else { return true }
        return scrollView.contentOffset.y <= 0
    }
}

2. Define Your Sheet's Dock Positions

Apple Maps' bottom sheet has 3 key states: collapsed, half-expanded, and full-expanded. Define these as constants to keep your code clean:

private let collapsedHeight: CGFloat = 60 // Adjust to your needs
private let halfExpandedHeight: CGFloat = UIScreen.main.bounds.height * 0.5
private let fullExpandedHeight: CGFloat = UIScreen.main.bounds.height - 80 // Account for navigation/status bars
private var startingSheetY: CGFloat = 0 // Track where the sheet was when the gesture started

3. Handle the Pan Gesture's States

The core logic lives in the gesture handler, where you'll manage the sheet's position during the drag, then snap it to the nearest dock position when the gesture ends:

@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
    let translation = gesture.translation(in: view.superview)
    
    switch gesture.state {
    case .began:
        // Record the sheet's starting Y position when the user starts dragging
        startingSheetY = view.frame.origin.y
    case .changed:
        // Calculate the new Y position, and clamp it to prevent the sheet from going off-screen
        let newY = startingSheetY + translation.y
        let clampedY = max(fullExpandedHeight, min(newY, UIScreen.main.bounds.height - collapsedHeight))
        view.frame.origin.y = clampedY
    case .ended, .cancelled:
        // Use velocity and current position to decide which dock state to snap to
        let velocity = gesture.velocity(in: view.superview).y
        let currentY = view.frame.origin.y
        
        var targetY: CGFloat
        // Prioritize velocity first for fast swipes
        if velocity < -300 { // Fast upward swipe
            targetY = fullExpandedHeight
        } else if velocity > 300 { // Fast downward swipe
            targetY = UIScreen.main.bounds.height - collapsedHeight
        } else {
            // Snap to the nearest dock position for slow drags
            let midHalfFull = (halfExpandedHeight + fullExpandedHeight) / 2
            let midCollapsedHalf = (UIScreen.main.bounds.height - collapsedHeight + halfExpandedHeight) / 2
            
            targetY = currentY < midHalfFull ? fullExpandedHeight : currentY < midCollapsedHalf ? halfExpandedHeight : UIScreen.main.bounds.height - collapsedHeight
        }
        
        // Animate the snap for that polished Apple-style feel
        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5) {
            self.view.frame.origin.y = targetY
        }
    default:
        break
    }
}

4. Set the Initial Sheet Position

Make sure when you add panVC to your mainVC, you set its initial frame to the collapsed state:

// In your mainVC where you add panVC
let panVC = YourPanVC()
addChild(panVC)
view.addSubview(panVC.view)
panVC.didMove(toParent: self)

// Set initial frame to collapsed state
let initialFrame = CGRect(
    x: 0,
    y: view.bounds.height - panVC.collapsedHeight,
    width: view.bounds.width,
    height: view.bounds.height
)
panVC.view.frame = initialFrame

Common Pitfalls to Avoid

  • Forgetting to clamp the sheet's position: If you don't limit the Y value, the sheet can be dragged off-screen entirely.
  • Ignoring gesture velocity: Without velocity checks, fast swipes won't feel responsive like Apple's implementation.
  • Gesture conflicts with internal scroll views: If your sheet has a table/collection view, make sure you only allow dragging the sheet when the scroll view is at the top (the delegate method above handles this).

Give these steps a shot—this should get you pretty close to that Apple Maps-style smooth bottom sheet. If you hit specific snags (like layout issues with Auto Layout instead of frame-based positioning), feel free to share more details!

内容的提问来源于stack exchange,提问作者Ekra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 09:40:32