UIImagePickerController editing view circle overlay
Asked Answered
P

5

17

I've been able to get pretty far with what I've been wanting to accomplish, and that's to replicate iOS's built in circular photo cropper for the built in contacts app. However, I'm stuck at trying to get my CAShapeLayers made correctly. I'm trying to make a transparent 320 px diameter circle and the rest of the view filled with an 0.9 alpha black background. The circle and rectangle are in the right place, but, the circle is not completely transparent like I need it to be.

I'm lost as to how to fix this. I appreciate your help! Code and screenshot:

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if ([navigationController.viewControllers count] == 3)
    {
        CGRect screenRect = [[UIScreen mainScreen] bounds];
        CGFloat screenHeight = screenRect.size.height;

        UIView *plCropOverlay = [[[viewController.view.subviews objectAtIndex:1]subviews] objectAtIndex:0];

        plCropOverlay.hidden = YES;

        CAShapeLayer *circleLayer = [CAShapeLayer layer];

        if (screenHeight == 568)
        {
            [circleLayer setPosition:CGPointMake(0.0f,124.0f)];
        }    
        else
        {
            [circleLayer setPosition:CGPointMake(0.0f,80.0f)];
        }

        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
                          CGRectMake(0.0f, 0.0f, 320.0f, 320.0f)];

        [circleLayer setPath:[path CGPath]];

        [circleLayer setFillColor:[[UIColor whiteColor] CGColor]];
        circleLayer.opacity = 0.7f;

        // Set to 0.7f to show for screenshot purposes; setting to 0.0 would make it invisible and blend in with the below rectangleLayer.

        CAShapeLayer *rectangleLayer = [CAShapeLayer layer];

        UIBezierPath *path2 = [UIBezierPath bezierPathWithRect:CGRectMake(0.0f, 0.0f, 320.0f, screenHeight - 72)];
        [rectangleLayer setPath:[path2 CGPath]];

        [rectangleLayer setFillColor:[[UIColor blackColor] CGColor]];
        [rectangleLayer setOpacity:0.9f];
        [rectangleLayer addSublayer:circleLayer];
        [[viewController.view layer] addSublayer:rectangleLayer];

        UILabel *moveLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, 320, 50)];
        [moveLabel setText:@"Move and Scale"];
        [moveLabel setTextAlignment:NSTextAlignmentCenter];
        [moveLabel setTextColor:[UIColor whiteColor]];

        [viewController.view addSubview:moveLabel];
    }
}

enter image description here

Purcell answered 27/12, 2013 at 3:8 Comment(2)
Have you try with cameraOverlayView property of UIImagePickerController ?Horselaugh
That won't work for my purpose. Please read the original question.Purcell
P
14

Resolved code:

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if ([navigationController.viewControllers count] == 3)
    {
        CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;

        UIView *plCropOverlay = [[[viewController.view.subviews objectAtIndex:1]subviews] objectAtIndex:0];

        plCropOverlay.hidden = YES;

        int position = 0;

        if (screenHeight == 568)
        {
            position = 124;
        }
        else
        {
            position = 80;
        }

        CAShapeLayer *circleLayer = [CAShapeLayer layer];

        UIBezierPath *path2 = [UIBezierPath bezierPathWithOvalInRect:
                           CGRectMake(0.0f, position, 320.0f, 320.0f)];
        [path2 setUsesEvenOddFillRule:YES];

        [circleLayer setPath:[path2 CGPath]];

        [circleLayer setFillColor:[[UIColor clearColor] CGColor]];
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 320, screenHeight-72) cornerRadius:0];

        [path appendPath:path2];
        [path setUsesEvenOddFillRule:YES];

        CAShapeLayer *fillLayer = [CAShapeLayer layer];
        fillLayer.path = path.CGPath;
        fillLayer.fillRule = kCAFillRuleEvenOdd;
        fillLayer.fillColor = [UIColor blackColor].CGColor;
        fillLayer.opacity = 0.8;
        [viewController.view.layer addSublayer:fillLayer];

        UILabel *moveLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, 320, 50)];
        [moveLabel setText:@"Move and Scale"];
        [moveLabel setTextAlignment:NSTextAlignmentCenter];
        [moveLabel setTextColor:[UIColor whiteColor]];

        [viewController.view addSubview:moveLabel];
    }
}
Purcell answered 27/12, 2013 at 21:48 Comment(8)
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated can be also used to avoid the delay for the resizing.any way great work .@"kudos....."Strickman
This must be a quoincidence, because I just discovered that method last night lol, awesome though and works wonderfully because technically the circle's bounds is a square so the user never knows.Purcell
How do you get the circular image after tapping on 'Choose' ? (Like Contacts app uses the circular image to show selected region)Soissons
How did you manage to move the view for cropping and editing?Doorstep
Do you know how to do this for the camera? With the navigationController delegate, I can access different views and layers when taking a picture with the camera ([navigationController.viewControllers count] == 1) but I can't when editing the image. Do you know how to do this? Thanks.Heartburning
@aviatorken89 I want circular cropper with UIImagePickerControllerSourceTypeCamera . I used above code for photolibrary and its working fine . Please help me if you have some code or idea please share with me. Please answer also on my question #32768711Crosscut
@MarieDm did you got any solution . if yes please share with me also I also want same with Camera .Thanks . Please answer also on my question #32768711Crosscut
@klcjr89: This works perfectly fine for iOS 10, but does not work for iOS 11. Please let me know the changes for iOS 11. Thanks.Predispose
R
3

I've changed the code of @aviatorken89 because it wasn't working on iPhone 6/6+ and iPad. Now it should work with any iPhone's screen size and also on iPad! Tested on iOS 7 and iOS 8.

All these methods aren't really reliable because they are based on the Image Picker subviews hierarchy, and of course Apple could change it. I've tried to protect the code as far as I could, in order to prevent possibile crashes on future iOS releases.

I'll try to keep my solution updated on a gist: https://gist.github.com/andreacipriani/74ea67db8f17673f1b8b

Here is the code:

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if ([navigationController.viewControllers count] == 3 && ([[[[navigationController.viewControllers objectAtIndex:2] class] description] isEqualToString:@"PUUIImageViewController"] || [[[[navigationController.viewControllers objectAtIndex:2] class] description] isEqualToString:@"PLUIImageViewController"]))

        [self addCircleOverlayToImagePicker:viewController];
    }
}

-(void)addCircleOverlayToImagePicker:(UIViewController*)viewController
{
    UIColor *circleColor = [UIColor clearColor];
    UIColor *maskColor = [[UIColor blackColor] colorWithAlphaComponent:0.8];

    CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
    CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width;

    UIView *plCropOverlayCropView; //The default crop overlay view, we wan't to hide it and show our circular one
    UIView *plCropOverlayBottomBar; //On iPhone this is the bar with "cancel" and "choose" buttons, on Ipad it's an Image View with a label saying "Scale and move"

    //Subviews hirearchy is different in iPad/iPhone:
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){

        plCropOverlayCropView = [viewController.view.subviews objectAtIndex:1];
        plCropOverlayBottomBar = [[[[viewController.view subviews] objectAtIndex:1] subviews] objectAtIndex:1];

        //Protect against iOS changes...
        if (! [[[plCropOverlayCropView class] description] isEqualToString:@"PLCropOverlay"]){
            DLog(@"Warning - Image Picker with circle overlay: PLCropOverlay not found");
            return;
        }
        if (! [[[plCropOverlayBottomBar class] description] isEqualToString:@"UIImageView"]){
            DLog(@"Warning - Image Picker with circle overlay: BottomBar not found");
            return;
        }
    }
    else{
        plCropOverlayCropView = [[[viewController.view.subviews objectAtIndex:1] subviews] firstObject];
        plCropOverlayBottomBar = [[[[viewController.view subviews] objectAtIndex:1] subviews] objectAtIndex:1];

        //Protect against iOS changes...
        if (! [[[plCropOverlayCropView class] description] isEqualToString:@"PLCropOverlayCropView"]){
            DDLogWarn(@"Image Picker with circle overlay: PLCropOverlayCropView not found");
            return;
        }
        if (! [[[plCropOverlayBottomBar class] description] isEqualToString:@"PLCropOverlayBottomBar"]){
            DDLogWarn(@"Image Picker with circle overlay: PLCropOverlayBottomBar not found");
            return;
        }
    }

    //It seems that everything is ok, we found the CropOverlayCropView and the CropOverlayBottomBar

    plCropOverlayCropView.hidden = YES; //Hide default CropView

    CAShapeLayer *circleLayer = [CAShapeLayer layer];
    //Center the circleLayer frame:
    UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0f, screenHeight/2 - screenWidth/2, screenWidth, screenWidth)];
    circlePath.usesEvenOddFillRule = YES;
    circleLayer.path = [circlePath CGPath];
    circleLayer.fillColor = circleColor.CGColor;
    //Mask layer frame: it begins on y=0 and ends on y = plCropOverlayBottomBar.origin.y
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, screenWidth, screenHeight- plCropOverlayBottomBar.frame.size.height) cornerRadius:0];
    [maskPath appendPath:circlePath];
    maskPath.usesEvenOddFillRule = YES;

    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.path = maskPath.CGPath;
    maskLayer.fillRule = kCAFillRuleEvenOdd;
    maskLayer.fillColor = maskColor.CGColor;
    [viewController.view.layer addSublayer:maskLayer];

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone){
        //On iPhone add an hint label on top saying "scale and move" or whatever you want
        UILabel *cropLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, screenWidth, 50)];
        [cropLabel setText:@"Scale and move"]; //You should localize it
        [cropLabel setTextAlignment:NSTextAlignmentCenter];
        [cropLabel setTextColor:[UIColor whiteColor]];
        [viewController.view addSubview:cropLabel];
    }
    else{ //On iPad re-add the overlayBottomBar with the label "scale and move" because we set its parent to hidden (it's a subview of PLCropOverlay)
        [viewController.view addSubview:plCropOverlayBottomBar];
    }
} 
Recent answered 19/5, 2015 at 15:41 Comment(3)
This code doesn't seem to work on iPad 2 and iPad Pro when UIImagePickerController is presented in a popover. Cropping area is wrong.Trowbridge
Your gist link is brokenSum
Thank you @Joey. I've updated the broken link (due to a change in my Github nickname)!Recent
C
1

Swift 3 version (also with rounded edit layer for pictures taken by camera):

// Insert this code to your view controller
private var editLayer: CAShapeLayer!
private var label: UILabel!


override func viewDidLoad()
{
    super.viewDidLoad()

    // Rounded edit layer
    navigationController?.delegate = self
    NotificationCenter.default.addObserver(self, selector: #selector(pictureCaptured), name: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidCaptureItem"), object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(pictureRejected), name: NSNotification.Name(rawValue: "_UIImagePickerControllerUserDidRejectItem"), object: nil)
}


@objc private func pictureCaptured()
{
    addRoundedEditLayer(to: ...your UIImagePickerController..., forCamera: true)
}


@objc private func pictureRejected()
{
    editLayer.removeFromSuperlayer()
    label.removeFromSuperview()
}


deinit
{
    NotificationCenter.default.removeObserver(self)
}    



// MARK: Navigation controller delegate

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
{
    // Image picker in edit mode
    if let imageVC = NSClassFromString("PUUIImageViewController")
    {
        if viewController.isKind(of: imageVC) {
            addRoundedEditLayer(to: viewController, forCamera: false)
        }
    }
}


private func addRoundedEditLayer(to viewController: UIViewController, forCamera: Bool)
{
    hideDefaultEditOverlay(view: viewController.view)

    // Circle in edit layer - y position
    let bottomBarHeight: CGFloat = 72.0
    let position = (forCamera) ? viewController.view.center.y - viewController.view.center.x - bottomBarHeight/2 : viewController.view.center.y - viewController.view.center.x

    let viewWidth = viewController.view.frame.width
    let viewHeight = viewController.view.frame.height

    let emptyShapePath = UIBezierPath(ovalIn: CGRect(x: 0, y: position, width: viewWidth, height: viewWidth))
    emptyShapePath.usesEvenOddFillRule = true

    let filledShapePath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight - bottomBarHeight), cornerRadius: 0)
    filledShapePath.append(emptyShapePath)
    filledShapePath.usesEvenOddFillRule = true

    editLayer = CAShapeLayer()
    editLayer.path = filledShapePath.cgPath
    editLayer.fillRule = kCAFillRuleEvenOdd
    editLayer.fillColor = UIColor.black.cgColor
    editLayer.opacity = 0.5
    viewController.view.layer.addSublayer(editLayer)

    // Move and Scale label
    label = UILabel(frame: CGRect(x: 0, y: 10, width: viewWidth, height: 50))
    label.text = "Move and Scale"
    label.textAlignment = .center
    label.textColor = UIColor.white
    viewController.view.addSubview(label)
}


private func hideDefaultEditOverlay(view: UIView)
{
    for subview in view.subviews
    {
        if let cropOverlay = NSClassFromString("PLCropOverlayCropView")
        {
            if subview.isKind(of: cropOverlay) {
                subview.isHidden = true
                break
            }
            else {
                hideDefaultEditOverlay(view: subview)
            }
        }
    }
}
Camphene answered 10/3, 2017 at 14:48 Comment(1)
It shows a circular overlay while browsing images from gallery, not after the image has been selected from gallery.Dismuke
D
0

For doing this from camera, try using the cameraOverlayView and set your own view. That will work only when picking from camera though and not photo library.

Demibastion answered 15/1, 2015 at 22:54 Comment(2)
how to add cameraoverlayView . Please answer also on my question #32768711. ThanksCrosscut
For photo library what can we use?Cilicia
R
0

In the code of Jakub Marek, there's an issue with persistent rounded layer if you open a second time the camera.

to solve it add in your openCamera func:

editLayer?.removeFromSuperlayer()
label?.removeFromSuperview()

and replace in private func hideDefaultEditOverlay(view: UIView)

subview.isHidden = true

by

subview.removeFromSuperview()

Code :

func openCamera(){
    if(UIImagePickerController .isSourceTypeAvailable(UIImagePickerController.SourceType.camera)){
        imagePicker.sourceType = UIImagePickerController.SourceType.camera
        //If you dont want to edit the photo then you can set allowsEditing to false
        imagePicker.allowsEditing = true
        imagePicker.cameraDevice = .rear
        imagePicker.showsCameraControls = true
        imagePicker.cameraCaptureMode = .photo

        imagePicker.delegate = self
        editLayer?.removeFromSuperlayer()
        label?.removeFromSuperview()
        self.present(imagePicker, animated: true, completion: nil)

    }
    else{
        let alert  = UIAlertController(title: NSLocalizedString("Attention",comment:""), message: NSLocalizedString("You don't have any camera",comment:""), preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: NSLocalizedString("OK",comment:""), style: .default, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
}

private func hideDefaultEditOverlay(view: UIView)
{
    for subview in view.subviews
    {
        if let cropOverlay = NSClassFromString("PLCropOverlayCropView")
        {
            if subview.isKind(of: cropOverlay) {
                subview.removeFromSuperview()
                //subview.isHidden = true
                break
            }
            else {
                hideDefaultEditOverlay(view: subview)
            }
        }
    }
}
Retroflex answered 22/9, 2019 at 4:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.