0

I'm trying to mask a UIImageView in such a way that it would allow the user to drag the image around without moving its mask. The effect would be similar to how one can position an image within the Instagram app essentially allowing the user to define the crop region of the image.

Here's an animated gif to demonstrate what I'm after.

enter image description here

Here's how I'm currently masking the image and repositioning it on drag/pan events.

import UIKit

class ViewController: UIViewController {

    var dragDelta = CGPoint()
    @IBOutlet weak var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()
        attachMask()
    // listen for pan/drag events //
        let pan = UIPanGestureRecognizer(target:self, action:#selector(onPanGesture))
        pan.maximumNumberOfTouches = 1
        pan.minimumNumberOfTouches = 1
        self.view.addGestureRecognizer(pan)
    }

    func onPanGesture(gesture:UIPanGestureRecognizer)
    {
        let point:CGPoint = gesture.locationInView(self.view)
        if (gesture.state == .Began){
            print("begin", point)
        // capture our drag start position
            dragDelta = CGPoint(x:point.x-imageView.frame.origin.x, y:point.y-imageView.frame.origin.y)
        }   else if (gesture.state == .Changed){
        // update image position based on how far we've dragged from drag start
            imageView.frame.origin.y = point.y - dragDelta.y
        }   else if (gesture.state == .Ended){
            print("ended", point)
        }
    }

    func attachMask()
    {
        let mask = CAShapeLayer()
        mask.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 100, width: imageView.frame.size.width, height: 400), cornerRadius: 5).CGPath
        mask.anchorPoint = CGPoint(x: 0, y: 0)
        mask.fillColor = UIColor.redColor().CGColor
        view.layer.addSublayer(mask)
        imageView.layer.mask = mask;
    }
}

This results in both the image and mask moving together as you see below. Any suggestions on how to "lock" the mask so the image can be moved independently underneath it would be very much appreciated.

enter image description here

braitsch
  • 14,906
  • 5
  • 42
  • 37

3 Answers3

2

Moving a mask and frame separately from each other to reach this effect isn't the best way to go about doing this. Most apps that do this sort of effect do the following:

  1. Add a UIScrollView to the root view (with panning/zooming enabled)
  2. Add a UIImageView to the UIScrollView
  3. Size the UIImageView such that it has a 1:1 ratio with the image
  4. Set the contentSize of the UIScrollView to match that of the UIImageView

The user can now pan around and zoom into the UIImageView as needed.

Next, if you're, say, cropping the image:

  1. Get the visible rectangle (taken from Getting the visible rect of an UIScrollView's content)

    CGRect visibleRect = [scrollView convertRect:scrollView.bounds toView:zoomedSubview];

  2. Use whatever cropping method you'd like on the UIImage to get the necessary content.

This is the smoothest way to handle this kind of interaction and the code stays pretty simple!

Community
  • 1
  • 1
AlexKoren
  • 1,605
  • 16
  • 28
  • 1
    Thanks, I took a look at this approach this morning and I think I've got it working. (see my additional answer below). I agree this certainly requires fewer lines of code although I don't know that I agree that it's necessarily a better approach as my original solution appears to allow for greater control over the shape of the mask than a UIScrollView offers unless I'm missing something? – braitsch May 18 '16 at 22:47
  • I was under the impression that you only wanted to have a rectangular shape. If not, you can always use masks to manipulate the shape within the UIScrollView. You can make a mask and then subtract it from the UIImageView – AlexKoren May 25 '16 at 22:02
1

Just figured it out. Setting the CAShapeLayer's position property to the inverse of the UIImageView's position as it's dragged will lock the CAShapeLayer in its original position however CoreAnimation by default will attempt to animate it whenever its position is reassigned.

This can be disabled by wrapping both position settings within a CATransaction as shown below.

func onPanGesture(gesture:UIPanGestureRecognizer)
{
    let point:CGPoint = gesture.locationInView(self.view)
    if (gesture.state == .Began){
        print("begin", point)
    // capture our drag start position
        dragDelta = CGPoint(x:point.x-imageView.frame.origin.x, y:point.y-imageView.frame.origin.y)
    }   else if (gesture.state == .Changed){
    // update image & mask positions based on the distance dragged 
    // and wrap both assignments in a CATransaction transaction to disable animations
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        mask.position.y = dragDelta.y - point.y
        imageView.frame.origin.y = point.y - dragDelta.y
        CATransaction.commit()
    }   else if (gesture.state == .Ended){
        print("ended", point)
    }
}

UPDATE

Here's an implementation of what I believe AlexKoren is suggesting. This approach nests a UIImageView within a UIScrollView and uses the UIScrollView to mask the image.

class ViewController: UIViewController, UIScrollViewDelegate {

    @IBOutlet weak var scrollView: UIScrollView!

    var imageView:UIImageView = UIImageView()

    override func viewDidLoad() {
        super.viewDidLoad()

        let image = UIImage(named: "point-bonitas")
        imageView.image = image
        imageView.frame = CGRectMake(0, 0, image!.size.width, image!.size.height);

        scrollView.delegate = self
        scrollView.contentMode = UIViewContentMode.Center
        scrollView.addSubview(imageView)
        scrollView.contentSize = imageView.frame.size

        let scale = scrollView.frame.size.width / scrollView.contentSize.width
        scrollView.minimumZoomScale = scale
        scrollView.maximumZoomScale = scale // set to 1 to allow zoom out to 100% of image size //
        scrollView.zoomScale = scale

    // center image vertically in scrollview //
        let offsetY:CGFloat = (scrollView.contentSize.height - scrollView.frame.size.height) / 2;
        scrollView.contentOffset = CGPointMake(0, offsetY);
    }

    func scrollViewDidZoom(scrollView: UIScrollView) {
        print("zoomed")
    }

    func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
        return imageView
    }
}
braitsch
  • 14,906
  • 5
  • 42
  • 37
  • I wouldn't suggest doing it this way and I can almost guarantee it's not the way Instagram does it. I will submit an answer below to help describe the solution that's probably best! – AlexKoren May 17 '16 at 23:38
0

The other, perhaps simpler way would be to put the image view in a scroll view and let the scroll view manage it for you. It handles everything.

Duncan C
  • 128,072
  • 22
  • 173
  • 272