Search code examples
iosswiftuikit

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


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?


Solution

  • 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