1

I am stumped on how to get a certain behavior out of my experiment with UIPanGestureRecognizer.

When a user taps and holds an image, and it then intersects with another image, a behavior is triggered. In this case, the behavior is that the opacity of the green or red image goes to 100%.

Once a user continues dragging the image around the screen, and breaks the intersect with either the green or red image, I'd like its opacity to return to 50%. My control flow is getting ugly. I keep tweaking it, but am stumped to find the logic that gets me what I want.

visual

 @objc func handlePan(sender: UIPanGestureRecognizer) {

    let imageOneView = sender.view!
    let translation = sender.translation(in: view)

    switch sender.state {

    case .began, .changed:
        imageOneView.center = CGPoint(x: imageOneView.center.x + translation.x, y: imageOne.center.y + translation.y)
        sender.setTranslation(CGPoint.zero, in: view)
        view.bringSubviewToFront(imageOne)
        if imageOne.frame.intersects(winImage.frame) {
            winImage.alpha = 1
        } else {
            if imageOne.frame.intersects(loseImage.frame) {
                loseImage.alpha = 1
                winImage.alpha = 0.5

            } else {
                return
            }
        }

    case .ended:
        if imageOne.frame.intersects(winImage.frame) {
            UIView.animate(withDuration: 0.3, animations: {
                self.imageOne.alpha = 0.0
            })
            performSegue(withIdentifier: "winnerDetailView", sender: self)
        } else {
            if imageOne.frame.intersects(loseImage.frame) {
                UIView.animate(withDuration: 0.3, animations: {
                    self.imageOne.alpha = 0.0
                })
                navigationItem.rightBarButtonItem?.isEnabled = true
                loseImage.alpha = 0.5
            } else {
                UIView.animate(withDuration: 0.3) {
                    self.imageOne.frame.origin = self.imageOneOrgin
                }
            }
        }
    default:
        break
    }
}

And additional code: https://github.com/ericmseitz/imageWar

Thanks.

1 Answers1

0

Couple things...

  1. Control flow looks good.
  2. To get the "win" and "lost" alpha reset, set them both to 0.5 on entry to handlePan(). If intersection is detected, set the appropriate one to 1.0. It won't update until the UI updates, which won't happen inside this function.
  3. If you have your draggable views embedded in stack views (as you have in your GitHub repo), you won't be able to bring them in front of each other.
  4. Also, if you have your draggable views embedded in stack views (or other views) make sure to convert the frame to the main view's coordinate space.

Here is modified code, based on what you posted in your question and the layout (stack view embedded) in your repo:

class ViewController: UIViewController {

    // IBOutlets

    @IBOutlet weak var winImage: UIImageView!
    @IBOutlet weak var loseImage: UIImageView!

    @IBOutlet weak var imageOne: UIImageView!
    @IBOutlet weak var imageTwo: UIImageView!
    @IBOutlet weak var imageThree: UIImageView!
    @IBOutlet weak var imageFour: UIImageView!

    // Constants and Variables

    var currentImageCenter = CGPoint.zero

    override func viewDidLoad() {
        super.viewDidLoad()

        [imageOne, imageTwo, imageThree, imageFour].forEach {
            let p = UIPanGestureRecognizer(target: self, action: #selector(handlePan(sender:)))
            $0?.addGestureRecognizer(p)
        }

        winImage.alpha = 0.5
        loseImage.alpha = 0.5
    }

    @objc func handlePan(sender: UIPanGestureRecognizer) {

        guard let imageOneView = sender.view else { return }

        let translation = sender.translation(in: view)

        // set alpha for both to 0.5
        // if either is intersected, it will be set to 1.0
        winImage.alpha = 0.5
        loseImage.alpha = 0.5

        switch sender.state {

        case .began:
            // track center of drag-view so we can return if needed
            currentImageCenter = imageOneView.center

        case .changed:
            imageOneView.center = CGPoint(x: imageOneView.center.x + translation.x, y: imageOneView.center.y + translation.y)
            sender.setTranslation(CGPoint.zero, in: view)

            // convert drag-view frame to view coordinate space
            guard let r = view.getConvertedFrame(fromSubview: imageOneView) else { return }

            view.bringSubviewToFront(imageOneView)
            if r.intersects(winImage.frame) {
                winImage.alpha = 1
            } else {
                if r.intersects(loseImage.frame) {
                    loseImage.alpha = 1
                } else {
                    return
                }
            }

        case .ended:

            // convert drag-view frame to view coordinate space
            guard let r = view.getConvertedFrame(fromSubview: imageOneView) else { return }

            if r.intersects(winImage.frame) {
                UIView.animate(withDuration: 0.3, animations: {
                    imageOneView.alpha = 0.0
                })
                //performSegue(withIdentifier: "winnerDetailView", sender: self)
            } else {
                if r.intersects(loseImage.frame) {
                    UIView.animate(withDuration: 0.3, animations: {
                        imageOneView.alpha = 0.0
                    })
                    navigationItem.rightBarButtonItem?.isEnabled = true
                } else {
                    UIView.animate(withDuration: 0.3) {
                        imageOneView.center = self.currentImageCenter
                    }
                }
            }

        default:
            break
        }
    }
}

Note: the above needs this UIView extension to convert the frame coordinates:

// from Answer on StackOverflow: https://stackoverflow.com/a/57261226/6257435
extension UIView {

    // there can be other views between `subview` and `self`
    func getConvertedFrame(fromSubview subview: UIView) -> CGRect? {

        // check if `subview` is a subview of self
        guard subview.isDescendant(of: self) else {
            return nil
        }

        var frame = subview.frame
        if subview.superview == nil {
            return frame
        }

        var superview = subview.superview
        while superview != self {
            frame = superview!.convert(frame, to: superview!.superview)
            if superview!.superview == nil {
                break
            } else {
                superview = superview!.superview
            }
        }

        return superview!.convert(frame, to: self)
    }

}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • DonMag - This was a fantastic help. I'm still trying to grok it all, but even without really comprehending your guidance, I have been successful in making it work. Seriously, you made a big difference today and I am grateful for your time and expertise. – Eric M. Seitz Feb 07 '20 at 20:51