Search code examples
iosswiftmaskcore-image

Cut UIImage from UIView Mask


I have a UIView with a transparent maskLayerof certain radius at certain point. Besides that I have a UIImageView with UIPanGesture & UIPinchGesture.

Now I can drag or zoom UIImageView so that it can fit in the part of UIView mask. Once it is done I need to get UIImage from UIImageView with respect to transparent part.

I don't know how to achieve it.

masked image

Below is the code which creates overlay on a UIView with a specified mask CGRect & radius.

func createOverlay(frame: CGRect,
                       xOffset: CGFloat,
                       yOffset: CGFloat,
                       radius: CGFloat) -> UIView {
        // Step 1
    let overlayView = UIView(frame: frame)
    overlayView.backgroundColor = UIColor.groupTableViewBackground.withAlphaComponent(0.8)
    // Step 2
    let path = CGMutablePath()
    beizerPath = UIBezierPath(arcCenter: CGPoint(x: xOffset, y: yOffset + radius), radius: radius, startAngle: 0.0, endAngle: 2.0 * .pi, clockwise: false)
    path.addArc(center: CGPoint(x: xOffset, y: yOffset),
                     radius: radius,
                    startAngle: 0.0,
                    endAngle: 2.0 * .pi,
                    clockwise: false)
    path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
    // Step 3
    let maskLayer = CAShapeLayer()
    maskLayer.backgroundColor = UIColor.black.cgColor
    maskLayer.path = path
    maskLayer.fillRule = .evenOdd
    // Step 4
    overlayView.layer.mask = maskLayer
    overlayView.clipsToBounds = true

    return overlayView
}

Here I a storing UIBezierPath in case of any need!

Then, I tried clipping UIBezierPath on an UIImage but then it's an issue with the rect. Because, UIImageView can be dragged and zoom so it's CGRect changes. Below is the code I was using to create clipping.

extension UIImage {

    func imageByApplyingClippingBezierPath(_ path: UIBezierPath) -> UIImage {
        // Mask image using path
        let maskedImage = imageByApplyingMaskingBezierPath(path)

        // Crop image to frame of path
        let croppedImage = UIImage(cgImage: maskedImage.cgImage!.cropping(to: path.bounds)!)
        return croppedImage
    }

    func imageByApplyingMaskingBezierPath(_ path: UIBezierPath) -> UIImage {
        // Define graphic context (canvas) to paint on
        UIGraphicsBeginImageContext(size)
        let context = UIGraphicsGetCurrentContext()!
        context.saveGState()

        // Set the clipping mask
        path.addClip()
        draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))

        let maskedImage = UIGraphicsGetImageFromCurrentImageContext()!

        // Restore previous drawing context
        context.restoreGState()
        UIGraphicsEndImageContext()

        return maskedImage
    }

}

Other Solution, I think of taking snapshot of complete view and then, cut down the CGRect from it. But I don't think so that's the proper way to do so!


Solution

  • If possible it is easiest to create a round view and create a snapshot of that view. Check the following solution:

    func cutImageCircle(_ image: UIImage?, inFrame imageFrame: CGRect, contentMode: UIView.ContentMode = .scaleAspectFill, circle: (center: CGPoint, radius: CGFloat)) -> UIImage? {
        guard let image = image else { return nil }
    
        func generateSnapshotImage(ofView view: UIView, scale: CGFloat = 0.0) -> UIImage? {
            UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, scale)
    
            defer { UIGraphicsEndImageContext() }
    
            view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
            return UIGraphicsGetImageFromCurrentImageContext()
        }
    
        let imageViewPanel = UIView(frame: CGRect(x: 0.0, y: 0.0, width: circle.radius*2.0, height: circle.radius*2.0))
        imageViewPanel.clipsToBounds = true
        imageViewPanel.layer.cornerRadius = circle.radius
    
        let imageView = UIImageView(frame: {
            var frame = imageFrame
            frame.origin.x -= circle.center.x-circle.radius
            frame.origin.y -= circle.center.y-circle.radius
            return frame
        }())
        imageView.contentMode = contentMode
        imageView.image = image
    
        imageViewPanel.addSubview(imageView)
    
        let cutImage = generateSnapshotImage(ofView: imageViewPanel, scale: 1.0)
        return cutImage
    }
    

    You only need to compute where circle center is and what size it has. I added the option to adjust frame in there so you could control the size of output image. This way you can increase the quality of image taken.