How to crop image inside the circle in UIImageView in iOS

Solution 1:

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.

Solution 2:

Same code reused to play with Square, it may be helpful for other.

#import "ViewController.h"

@interface ViewController () <UIGestureRecognizerDelegate>

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@property (nonatomic) CGFloat circleRadius;
@property (nonatomic) CGPoint circleCenter;
@property (nonatomic) CGRect frame;


@property (nonatomic, weak) CAShapeLayer *maskLayer;
@property (nonatomic, weak) CAShapeLayer *circleLayer;

@property (nonatomic, weak) UIPinchGestureRecognizer *pinch;
@property (nonatomic, weak) UIPanGestureRecognizer   *pan;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // create layer mask for the image

    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    self.imageView.layer.mask = maskLayer;
    self.maskLayer = maskLayer;

    // create shape layer for circle we'll draw on top of image (the boundary of the circle)

    CAShapeLayer *circleLayer = [CAShapeLayer layer];
    circleLayer.lineWidth = 3.0;
    circleLayer.fillColor = [[UIColor clearColor] CGColor];
    circleLayer.strokeColor = [[UIColor blackColor] CGColor];
    [self.imageView.layer addSublayer:circleLayer];
    self.circleLayer = circleLayer;

    // create circle path

    [self updateCirclePathAtLocation:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0) radius:self.view.bounds.size.width * 0.30];

    // create pan gesture

    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    pan.delegate = self;
    [self.imageView addGestureRecognizer:pan];
    self.imageView.userInteractionEnabled = YES;
    self.pan = pan;

    // create pan gesture

    UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
    pinch.delegate = self;
    [self.view addGestureRecognizer:pinch];
    self.pinch = pinch;
}

- (void)updateCirclePathAtLocation:(CGPoint)location radius:(CGFloat)radius
{
    self.circleCenter = location;
    self.circleRadius = radius;
    self.frame =CGRectMake(self.circleCenter.x/2, self.circleCenter.y/2, self.circleRadius, self.circleRadius);
    UIBezierPath *path =     [UIBezierPath bezierPathWithRoundedRect:self.frame cornerRadius:0];
//    [path addArcWithCenter:self.circleCenter
//                    radius:self.circleRadius
//                startAngle:0.0
//                  endAngle:M_PI * 2.0
//                 clockwise:YES];


    self.maskLayer.path = [path CGPath];
    self.circleLayer.path = [path CGPath];
}

- (IBAction)didTouchUpInsideSaveButton:(id)sender
{
    NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *path = [documentsPath stringByAppendingPathComponent:@"image.png"];

    CGFloat scale  = [[self.imageView.window screen] scale];

    CGRect frame = CGRectMake(self.frame.origin.x *scale,
                              self.frame.origin.y *scale,
                              self.frame.size.width*scale,
                              self.frame.size.width*scale);

    // temporarily remove the circleLayer

    CALayer *circleLayer = self.circleLayer;
    [self.circleLayer removeFromSuperlayer];

    // render the clipped image

    UIGraphicsBeginImageContextWithOptions(self.imageView.frame.size, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    if ([self.imageView respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)])
    {
        // if iOS 7, just draw it

        [self.imageView drawViewHierarchyInRect:self.imageView.bounds afterScreenUpdates:YES];
    }
    else
    {
        // if pre iOS 7, manually clip it

        CGContextAddRect(context, self.frame);
        CGContextClip(context);
        [self.imageView.layer renderInContext:context];
    }

    // capture the image and close the context

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // add the circleLayer back

    [self.imageView.layer addSublayer:circleLayer];

    // crop the image
    NSLog(@"circle fram %@",NSStringFromCGRect(frame));
    NSLog(@"self fram %@",NSStringFromCGRect(self.frame));

    CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], frame);
    UIImage *croppedImage = [UIImage imageWithCGImage:imageRef];

    // save the image

    NSData *data = UIImagePNGRepresentation(croppedImage);
    [data writeToFile:path atomically:YES];

    // tell the user we're done

    [[[UIAlertView alloc] initWithTitle:nil message:@"Saved" delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
}


#pragma mark - Gesture recognizers

- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
    static CGPoint oldCenter;
    CGPoint tranlation = [gesture translationInView:gesture.view];

    if (gesture.state == UIGestureRecognizerStateBegan)
    {
        oldCenter = self.circleCenter;
    }

    CGPoint newCenter = CGPointMake(oldCenter.x + tranlation.x, oldCenter.y + tranlation.y);

    [self updateCirclePathAtLocation:newCenter radius:self.circleRadius];
}

- (void)handlePinch:(UIPinchGestureRecognizer *)gesture
{
    static CGFloat oldRadius;
    CGFloat scale = [gesture scale];

    if (gesture.state == UIGestureRecognizerStateBegan)
    {
        oldRadius = self.circleRadius;
    }

    CGFloat newRadius = oldRadius * scale;

    [self updateCirclePathAtLocation:self.circleCenter radius:newRadius];
}


#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ((gestureRecognizer == self.pan   && otherGestureRecognizer == self.pinch) ||
        (gestureRecognizer == self.pinch && otherGestureRecognizer == self.pan))
    {
        return YES;
    }

    return NO;
}

@end

Solution 3:

#import "ViewController.h"

@interface ViewController ()


 @property (weak, nonatomic) IBOutlet UIImageView *imageView;
 @property (weak, nonatomic) IBOutlet UIImageView *crppedImageView;


 @property (nonatomic) CGFloat circleRadius;
 @property (nonatomic) CGPoint circleCenter;

 @property (nonatomic, weak) CAShapeLayer *maskLayer;
 @property (nonatomic, weak) CAShapeLayer *maskSubLayer;
 @property (nonatomic, weak) CAShapeLayer *circleLayer;

 @property (nonatomic, weak) UIPinchGestureRecognizer *pinch;
 @property (nonatomic, weak) UIPanGestureRecognizer   *pan;


@end

@implementation ViewController


 - (void)viewDidLoad
{
 [super viewDidLoad];

// create layer mask for the image

CAShapeLayer *maskLayer = [CAShapeLayer layer];
CGRect maskRect = self.imageView.frame;

// Create a path with the rectangle in it.
CGPathRef path = CGPathCreateWithRect(maskRect, NULL);

      // Set the path to the mask layer.
       maskLayer.path = path;

   //    maskLayer.fillRule = kCAFillRuleEvenOdd;
  //    maskLayer.fillColor = [UIColor blueColor].CGColor;
  //    maskLayer.opacity = 0.5;

self.imageView.layer.mask = maskLayer;
self.maskLayer = maskLayer;

CAShapeLayer *maskLayer1 = [CAShapeLayer layer];
CGRect maskRect1 = self.imageView.frame;

// Create a path with the rectangle in it.
CGPathRef path1 = CGPathCreateWithRect(maskRect1, NULL);

// Set the path to the mask layer.
maskLayer1.path = path1;

[self.imageView.layer.mask addSublayer:maskLayer1];
self.maskSubLayer = maskLayer1;


// create shape layer for circle we'll draw on top of image (the boundary of the circle)

CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.lineWidth = 3.0;
circleLayer.fillColor = [[UIColor blueColor] CGColor];
circleLayer.fillRule = kCAFillRuleEvenOdd;
circleLayer.opacity = 0.5;
circleLayer.strokeColor = [[UIColor blackColor] CGColor];
[self.imageView.layer addSublayer:circleLayer];
self.circleLayer = circleLayer;

// create circle path

[self updateCirclePathAtLocation:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0) radius:self.view.bounds.size.width * 0.30];

// create pan gesture

UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
pan.delegate = self;
[self.imageView addGestureRecognizer:pan];
self.imageView.userInteractionEnabled = YES;
self.pan = pan;

// create pan gesture

UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
pinch.delegate = self;
[self.view addGestureRecognizer:pinch];
self.pinch = pinch;
 }

    - (void)updateCirclePathAtLocation:(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];

UIBezierPath *path1 = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.imageView.bounds.size.width, self.imageView.bounds.size.height) cornerRadius:0];
[path1 appendPath:path];
[path1 setUsesEvenOddFillRule:YES];


self.maskSubLayer.path = [path1 CGPath];
    self.circleLayer.path = [path1 CGPath];

}

    - (IBAction)didTouchUpInsideSaveButton:(id)sender
   {
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *path = [documentsPath stringByAppendingPathComponent:@"image.png"];

CGFloat scale  = [[self.imageView.window screen] scale];
CGFloat radius = self.circleRadius * scale;
CGPoint center = CGPointMake(self.circleCenter.x * scale, self.circleCenter.y * scale);

CGRect frame = CGRectMake(center.x - radius,
                          center.y - radius,
                          radius * 2.0,
                          radius * 2.0);

// temporarily remove the circleLayer

CALayer *circleLayer = self.circleLayer;
[self.circleLayer removeFromSuperlayer];

// render the clipped image

UIGraphicsBeginImageContextWithOptions(self.imageView.frame.size, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
if ([self.imageView respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)])
{
    // if iOS 7, just draw it

    [self.imageView drawViewHierarchyInRect:self.imageView.bounds afterScreenUpdates:YES];
}
else
{
    // if pre iOS 7, manually clip it

    CGContextAddArc(context, self.circleCenter.x, self.circleCenter.y, self.circleRadius, 0, M_PI * 2.0, YES);
    CGContextClip(context);
    [self.imageView.layer renderInContext:context];
}

// capture the image and close the context

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// add the circleLayer back

[self.imageView.layer addSublayer:circleLayer];

// crop the image

CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], frame);
UIImage *croppedImage = [UIImage imageWithCGImage:imageRef];

_crppedImageView.layer.cornerRadius = _crppedImageView.frame.size.height /2;
_crppedImageView.layer.masksToBounds = YES;
_crppedImageView.layer.borderWidth = 0;
self.crppedImageView.image = croppedImage;
// save the image

NSData *data = UIImagePNGRepresentation(croppedImage);
[data writeToFile:path atomically:YES];

// tell the user we're done

[[[UIAlertView alloc] initWithTitle:nil message:@"Saved" delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
 }


    #pragma mark - Gesture recognizers

      - (void)handlePan:(UIPanGestureRecognizer *)gesture
 {
static CGPoint oldCenter;
CGPoint tranlation = [gesture translationInView:gesture.view];

if (gesture.state == UIGestureRecognizerStateBegan)
{
    oldCenter = self.circleCenter;
}

CGPoint newCenter = CGPointMake(oldCenter.x + tranlation.x, oldCenter.y + tranlation.y);

[self updateCirclePathAtLocation:newCenter radius:self.circleRadius];
 }

  - (void)handlePinch:(UIPinchGestureRecognizer *)gesture
 {
static CGFloat oldRadius;
CGFloat scale = [gesture scale];

if (gesture.state == UIGestureRecognizerStateBegan)
{
    oldRadius = self.circleRadius;
}

CGFloat newRadius = oldRadius * scale;

[self updateCirclePathAtLocation:self.circleCenter radius:newRadius];
}


 #pragma mark - UIGestureRecognizerDelegate

    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
   {
      if ((gestureRecognizer == self.pan   && otherGestureRecognizer == self.pinch) ||
    (gestureRecognizer == self.pinch && otherGestureRecognizer == self.pan))
{
    return YES;
}

return NO;
 }

   - (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
 }

   @end

Solution 4:

enter image description here

In Swift 5 and SwiftUI this is how I solved it:

//
//  FlashlightView.swift
//
//  Created by Pascal Reitermann on 15.01.22.
//  Copyright 2022 Pascal Reitermann.
//

import SwiftUI

struct FlashlightView: View {
    let rect = UIScreen.main.bounds
    @State var center: CGPoint = CGPoint(x: UIScreen.screenWidth / 2, y: UIScreen.screenHeight / 2)

    var body: some View {
        ZStack {
            Rectangle()
                .fill(Color.black)
                .opacity(0.85)
                .mask(
                    holeShapeMask(in: rect)
                        .fill(style: FillStyle(eoFill: true))
                )
                .gesture(
                    dragLight
                )
        }
    }

    var dragLight: some Gesture {
        DragGesture()
            .onChanged { value in
                self.center = value.location
            }
    }

    func holeShapeMask(in rect: CGRect) -> Path {
        var path = Rectangle()
            .path(in: rect)
        if trunk.showLightBeam {
            path.move(to: center)
            path.addArc(
                center: center, radius: rect.width / 7, startAngle: .degrees(0), endAngle: .degrees(360),
                clockwise: false
            )
        }
        return path
    }

}

extension UIScreen {
   static let screenWidth = UIScreen.main.bounds.size.width
   static let screenHeight = UIScreen.main.bounds.size.height
}

You can use it in another class like this:

ZStack {
    Image("Background")
        .resizable()
        .aspectRatio(contentMode: .fill)
        .ignoresSafeArea()
    FlashlightView()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .ignoresSafeArea()
}