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:
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:
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()
}