Search code examples
iosuiimageview

How to crop image inside the circle in UIImageView in iOS


I have an app where I have a UIImageView which displays main image and another UIImageView being used as a mask which shows a circle which is transparent and outside its opaque, this circle can be moved using a UIPanGestureRecognizer, I want to know a wayout to crop the image inside the circle into a new image. Here is the attached code and screen shot

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    // create pan gesture

    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self
                                                                          action:@selector(handlePan:)];
    [self.view addGestureRecognizer:pan];


    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = [[self makeCircleAtLocation:self.view.center radius:100.0] CGPath];
    shapeLayer.strokeColor = [[UIColor clearColor] CGColor];
    shapeLayer.fillColor = nil;
    shapeLayer.lineWidth = 3.0;

    // Add CAShapeLayer to our view

    [self.view.layer addSublayer:shapeLayer];

    // Save this shape layer in a class property for future reference,
    // namely so we can remove it later if we tap elsewhere on the screen.

    self.circleLayer = shapeLayer;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
// Create a UIBezierPath which is a circle at a certain location of a certain radius.
// This also saves the circle's center and radius to class properties for future reference.

- (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius
{
    self.circleCenter = location;
    self.circleRadius = radius;

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path addArcWithCenter:self.circleCenter
                    radius:self.circleRadius
                startAngle:0.0
                  endAngle:M_PI * 2.0
                 clockwise:YES];

    return path;
}

- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
    static CGPoint oldCenter;

    if (gesture.state == UIGestureRecognizerStateBegan)
    {
        // If we're starting a pan, make sure we're inside the circle.
        // So, calculate the distance between the circle's center and
        // the gesture start location and we'll compare that to the
        // radius of the circle.

        CGPoint location = [gesture locationInView:gesture.view];
        CGPoint translation = [gesture translationInView:gesture.view];
        location.x -= translation.x;
        location.y -= translation.y;

        CGFloat x = location.x - self.circleCenter.x;
        CGFloat y = location.y - self.circleCenter.y;
        CGFloat distance = sqrtf(x*x + y*y);

        // If we're outside the circle, cancel the gesture.
        // If we're inside it, keep track of where the circle was.


        oldCenter = self.circleCenter;
    }
    else if (gesture.state == UIGestureRecognizerStateChanged)
    {
        // Let's calculate the new center of the circle by adding the
        // the translationInView to the old circle center.

        CGPoint translation = [gesture translationInView:gesture.view];
        CGPoint newCenter = CGPointMake(oldCenter.x + translation.x, oldCenter.y + translation.y);

      //  CGPoint newCenter = [gesture locationInView:self.view];
        if (newCenter.x < 160) {
            newCenter.x = 160;
        }
        else if (newCenter.x > self.view.frame.size.width - 160) {
            newCenter.x = self.view.frame.size.width - 160;
        }
        if (newCenter.y < 242) {
            newCenter.y = 242;
        }
        else if (newCenter.y > self.view.frame.size.height - imageMain.center.y) {
            newCenter.y = self.view.frame.size.height - imageMain.center.y;
        }

        // Update the path for our CAShapeLayer

       // self.circleLayer.path = [[self makeCircleAtLocation:newCenter radius:self.circleRadius] CGPath];
        imageCircle.center = newCenter;

    }
}

@end

And the result is

enter image description here


Solution

  • To save the masked image, one would use drawHierarchy(in:afterScreenUpdates:). You might also want to crop the image with cropping(to:). See my handleTap below for an example.

    But I note that you are apparently masking by overlaying an image. I might suggest using a UIBezierPath for the basis of both a layer mask for the image view, as well as the CAShapeLayer you'll use to draw the circle (assuming you want a border as you draw the circle. If your mask is a regular shape (such as a circle), it's probably more flexible to make it a CAShapeLayer with a UIBezierPath (rather than an image), because that way you can not only move it around with a pan gesture, but also scale it, too, with a pinch gesture:

    enter image description here

    Here is a sample implementation:

    //  ViewController.swift
    //  CircleMaskDemo
    //
    //  Created by Robert Ryan on 4/18/18.
    //  Copyright © 2018-2022 Robert Ryan. All rights reserved.
    //
    
    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var imageView: UIImageView!
        @IBOutlet weak var pinch: UIPinchGestureRecognizer!
        @IBOutlet weak var pan: UIPanGestureRecognizer!
    
        private let maskLayer = CAShapeLayer()
    
        private lazy var radius: CGFloat = min(view.bounds.width, view.bounds.height) * 0.3
        private lazy var center: CGPoint = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
    
        private let pathLayer: CAShapeLayer = {
            let _pathLayer = CAShapeLayer()
            _pathLayer.fillColor = UIColor.clear.cgColor
            _pathLayer.strokeColor = UIColor.black.cgColor
            _pathLayer.lineWidth = 3
            return _pathLayer
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            imageView.layer.addSublayer(pathLayer)
            imageView.layer.mask = maskLayer
            imageView.isUserInteractionEnabled = true
            imageView.addGestureRecognizer(pinch)
            imageView.addGestureRecognizer(pan)
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
    
            updateCirclePath(at: center, radius: radius)
        }
    
        private var oldCenter: CGPoint!
        private var oldRadius: CGFloat!
    }
    
    // MARK: - Actions
    
    extension ViewController {
        @IBAction func handlePinch(_ gesture: UIPinchGestureRecognizer) {
            let scale = gesture.scale
    
            if gesture.state == .began { oldRadius = radius }
    
            updateCirclePath(at: center, radius: oldRadius * scale)
        }
    
        @IBAction func handlePan(_ gesture: UIPanGestureRecognizer) {
            let translation = gesture.translation(in: gesture.view)
    
            if gesture.state == .began { oldCenter = center }
    
            let newCenter = CGPoint(x: oldCenter.x + translation.x, y: oldCenter.y + translation.y)
    
            updateCirclePath(at: newCenter, radius: radius)
        }
    
        @IBAction func handleTap(_ gesture: UITapGestureRecognizer) {
            let fileURL = try! FileManager.default
                .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                .appendingPathComponent("image.png")
    
            let scale  = imageView.window!.screen.scale
            let radius = self.radius * scale
            let center = CGPoint(x: self.center.x * scale, y: self.center.y * scale)
    
            let frame = CGRect(x: center.x - radius,
                               y: center.y - radius,
                               width: radius * 2.0,
                               height: radius * 2.0)
    
            // temporarily remove the circleLayer
    
            let saveLayer = pathLayer
            saveLayer.removeFromSuperlayer()
    
            // render the clipped image
    
            let image = UIGraphicsImageRenderer(size: imageView.frame.size).image { _ in
                imageView.drawHierarchy(in: imageView.bounds, afterScreenUpdates: true)
            }
    
            // add the circleLayer back
    
            imageView.layer.addSublayer(saveLayer)
    
            // crop the image
    
            guard
                let imageRef = image.cgImage?.cropping(to: frame),
                let data = UIImage(cgImage: imageRef).pngData()
            else {
                return
            }
    
            // save the image
    
            try? data.write(to: fileURL)
    
            // tell the user we're done
    
            let alert = UIAlertController(title: nil, message: "Saved", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            present(alert, animated: true)
        }
    }
    
    // MARK: - Private utility methods
    
    private extension ViewController {
        func updateCirclePath(at center: CGPoint, radius: CGFloat) {
            self.center = center
            self.radius = radius
    
            let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
            maskLayer.path = path.cgPath
            pathLayer.path = path.cgPath
        }
    }
    
    // MARK: - UIGestureRecognizerDelegate
    
    extension ViewController: UIGestureRecognizerDelegate {
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            let tuple = (gestureRecognizer, otherGestureRecognizer)
            return tuple == (pan, pinch) || tuple == (pinch, pan)
        }
    }
    

    If you don't want to draw the border around the circle, then it's even easier, as you can pull anything related to circleLayer.

    If you're interested in Objective-C example, see previous revision of this answer.