1

I'm using this code to make a UIViewController pannable take from this so post.

class ViewControllerPannable: UIViewController {
  var panGestureRecognizer: UIPanGestureRecognizer?
  var originalPosition: CGPoint?
  var currentPositionTouched: CGPoint?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
    view.addGestureRecognizer(panGestureRecognizer!)
  }
  
  func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
    let translation = panGesture.translation(in: view)
    
    if panGesture.state == .began {
      originalPosition = view.center
      currentPositionTouched = panGesture.location(in: view)
    } else if panGesture.state == .changed {
        view.frame.origin = CGPoint(
          x: translation.x,
          y: translation.y
        )
    } else if panGesture.state == .ended {
      let velocity = panGesture.velocity(in: view)

      if velocity.y >= 1500 {
        UIView.animate(withDuration: 0.2
          , animations: {
            self.view.frame.origin = CGPoint(
              x: self.view.frame.origin.x,
              y: self.view.frame.size.height
            )
          }, completion: { (isCompleted) in
            if isCompleted {
              self.dismiss(animated: false, completion: nil)
            }
        })
      } else {
        UIView.animate(withDuration: 0.2, animations: {
          self.view.center = self.originalPosition!
        })
      }
    }
  }
}

This works great on older phones that don't have a notch. But if the phone has a notch, once you start panning, any views pinned to the safe area jump to the superview. I think the issue is in the

view.frame.origin = CGPoint(
              x: translation.x,
              y: translation.y
            )

But I'm not sure how to make anything that was pinned to the safe area stay that way when panning.

Asperi
  • 228,894
  • 20
  • 464
  • 690
Micro
  • 10,303
  • 14
  • 82
  • 120

1 Answers1

0

You want to avoid layoutSubviews after you drag the view outside the superview's frame. As long as you are not calling setNeedsLayout manually, you can achieve this by ensuring that last frame.size change happens before crossing outside. Just make enough variables to precisely control for this situation.

I encountered this issue with a custom action-sheet-like presentation. When I pulled the view down to dismiss, there was no problem. But when I pulled the view up to stretch and then down, the safeAreaInsets jumped from 0 to non-zero suddenly.

The reason was because my view stretched upwards but not downwards. Pull up was frame.origin.y change and a frame.size.height change. Downwards was only a frame.origin.y change. However, UIPanGestureRecognizer does not have infinite granularity, and so the jump from negative to positive translation caused origin.y to change at the same time as frame.size.height in the downward direction. This triggered layoutSubviews while the view was partially outside the superview, which meant it ran layout code that suddenly acknowledged a safeAreaInsets change.

My solution is essentially:

if translation < 0 && oldTranslation > 0 || translation > 0 && oldTranslation < 0 {
   view.frame = originalFrame
} else {
   view.frame = calculate(translation)
}

This forces the view to be a static size before it crosses the superview's boundary, ensuring that layoutSubviews is called with the correct safeAreaInsets, but as the position changes, the safeArea does not.

Your situation may be a little different if you have something else triggering layoutSubviews. You might have to hunt that down.

Another thing I notice is that you're moving the UIViewController's view when it might have a parent, which could have other implications or edge cases because safeArea is inherited from the UIViewController hierarchy rather than solely superview.

xtravar
  • 1,321
  • 11
  • 24