How can I move a UIView to another parent while panning?

Viewed 242

I'm working on a game prototype in Swift using UIKit and SpriteKit. The inventory (at the bottom of this screenshot) is a UIView with UIImageView subviews for the individual items. In this example, a single acorn.

Game View

I have the acorn recognizing the "pan" gesture so I can drag it around. However, it renders it below the other views in the hierarchy. I want it to pop out of the inventory and be on top of everything (even above its parent view) so I can drop it onto other views elsewhere in the game.

This is what I have as my panHandler on the acorn view:

  @objc func panHandler(gesture: UIPanGestureRecognizer){
    switch (gesture.state) {
    case .began:
      removeFromSuperview()
      controller.view.addSubview(self)
    case .changed:
      let translation = gesture.translation(in: controller.view)
      if let v = gesture.view {
        v.center = CGPoint(x: v.center.x + translation.x, y: v.center.y + translation.y)
      }
      gesture.setTranslation(CGPoint.zero, in: controller.view)
    default:
      return
    }
  }

The problem is in the .began case, when I remove it from the superview, the pan gesture immediately cancels. Is it possible to remove the view from a superview and add it as a subview elsewhere while maintaining the pan gesture?

Or, if my approach is completely wrong, could you give me pointers how to accomplish my goal with another method?

1 Answers

The small answer is you can keep the gesture working if you don't call removeFromSuperview() on your view and add it as a subview right away to your controller view, but that's not the right way to do this because if the user cancels the drag you will have to re add to your main view again and if that view your dragging is heavy somehow it gets laggy and messy quickly

The long answer, and in my opinion is the right way to do it and what apple actually does in all drag and drop apis is

you can actually make a snapshot of the view you want to drag and add it as a subview of the controller view that's holding all your views and then call bringSubviewToFront(_ view: UIView) to make sure it's the top most view in the hierarchy and pass in the snapshot you took of the dragging view

in the .began you can hide the original view and in the .ended you can show it again

and also on .ended you can either take that snapshot and add to the dropping view or do anything else with it's dropping coordinates

I made a sample project to apply this Here is the storyboard design and view hierarchy

Here is the ViewController code

class ViewController: UIViewController {
    
    @IBOutlet var topView: UIView!
    @IBOutlet var bottomView: UIView!
    
    @IBOutlet var smallView: UIView!
    
    var snapshotView: UIView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let pan = UIPanGestureRecognizer(target: self, action: #selector(panning(_:)))
        smallView.addGestureRecognizer(pan)
    }
    
    @objc private func panning(_ pan: UIPanGestureRecognizer) {
        switch pan.state {
        case .began:
            smallView.backgroundColor = .systemYellow
            snapshotView = smallView.snapshotView(afterScreenUpdates: true)
            smallView.backgroundColor = .white
            
            if let snapshotView = self.snapshotView {
                self.snapshotView = snapshotView
                view.addSubview(snapshotView)
                view.bringSubviewToFront(snapshotView)
                snapshotView.backgroundColor = .blue
                snapshotView.center = bottomView.convert(smallView.center, to: view)
            }
            
        case .changed:
            guard let snapshotView = snapshotView else {
                fallthrough
            }
            
            smallView.alpha = 0
            
            let translation = pan.translation(in: view)
            snapshotView.center = CGPoint(x: snapshotView.center.x + translation.x, y: snapshotView.center.y + translation.y)
            pan.setTranslation(.zero, in: view)
            
        case .ended:
            if let snapshotView = snapshotView {
                let frame = view.convert(snapshotView.frame, to: topView)
                if topView.frame.contains(frame) {
                    topView.addSubview(snapshotView)
                    snapshotView.frame = frame
                    
                    smallView.alpha = 1
                } else {
                    bottomView.addSubview(snapshotView)
                    let newFrame = view.convert(snapshotView.frame, to: bottomView)
                    snapshotView.frame = newFrame
                    UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
                        snapshotView.frame = self.smallView.frame
                    } completion: { _ in
                        self.snapshotView?.removeFromSuperview()
                        self.snapshotView = nil
                        self.smallView.alpha = 1
                    }
                }
            }
            
        default: break
        }
    }
}

Here is how it ended up

enter image description here

Related