Search code examples
swiftuiimageviewcrop

How to crop UIImage to mask in Swift


I have a UIImage that I mask using another UIImage. The only problem is area outside the masked UIImage is still user interactable. How do I completely crop a UIImage to another image instead of mask.

@IBOutlet weak var imageView: UIImageView!

override func viewDidLoad() {
    super.viewDidLoad()
    let imageMask = UIImageView()

    imageMask.image = //image to mask

    imageMask.frame = photoImageView.bounds
    imageView.mask = imageMask
}

enter image description here enter image description here


Solution

  • Criteria

    A simple test case could define a background color for the view of the ViewController and load the image and mask. Then a UITapGestureRecognizer is added to the ViewController view and to the UIImageView.

    When applying a background color to the ViewController view, it is easy to see if masking works.

    If you then tap on a non-transparent area, the tap should be received by the UIImageView, otherwise the ViewController view should receive the tap.

    Image and Mask Image Size

    In most cases, the image and mask image size or at least the aspect ratio of the image and mask image is the same. It makes sense to use the same contentMode for the masking of UIImageView as for the original UIImageView, otherwise there would be a misalignment when changing the content mode in InterfaceBuilder at the latest.

    Test Case

    Therefore the test case could look like this:

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var imageView: UIImageView!
        private let maskView = UIImageView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.imageView.image = UIImage(named: "testimage")
            self.maskView.image = UIImage(named: "mask")
            self.imageView.mask = maskView
    
            let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(backgroundTapped))
            self.view.addGestureRecognizer(tapGestureRecognizer)
    
            let imageViewGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(iamgeViewTapped))
            self.imageView.addGestureRecognizer(imageViewGestureRecognizer)
        }
    
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            self.maskView.contentMode = self.imageView.contentMode
            self.maskView.frame = self.imageView.bounds
        }
    
        @objc private func backgroundTapped() {
            print ("background tapped!")
        }
    
        @objc private func iamgeViewTapped() {
            print ("image view tapped!")
        }
    
    }
    

    This code is already running. As expected, however, taps on the transparent area of the UIImageView also get here.

    CustomImageView

    Therefore we need a CustomImageView, which returns when clicking on a transparent pixel that it is not responsible for it.

    This can be achieved by overriding this method:

    func point(inside point: CGPoint, 
          with event: UIEvent?) -> Bool
    

    see documentation here: https://developer.apple.com/documentation/uikit/uiview/1622533-point

    Returns a Boolean value indicating whether the receiver contains the specified point.

    There is this cool answer already on SO, that is just slightly adapted: https://stackoverflow.com/a/27923457

    import UIKit
    
    class CustomImageView: UIImageView {
    
        override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
            return self.alphaFromPoint(point: point) > 32
        }
    
        private func alphaFromPoint(point: CGPoint) -> UInt8 {
            var pixel: [UInt8] = [0, 0, 0, 0]
            let colorSpace = CGColorSpaceCreateDeviceRGB();
            let alphaInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
            if let context = CGContext(data: &pixel,
                                       width: 1,
                                       height: 1,
                                       bitsPerComponent: 8,
                                       bytesPerRow: 4,
                                       space: colorSpace,
                                       bitmapInfo: alphaInfo.rawValue) {
                context.translateBy(x: -point.x, y: -point.y)
                self.layer.render(in: context)
            }
            return pixel[3]
        }
    
    }
    

    Don't forget to change the custom class of ImageView to CustomImageView in Xcode in the identity inspector.

    If you now tap on transparent areas, the view of the ViewController in the background gets the tap. If you tap on non-transparent areas our image view receives the tap.

    Demo

    Here is a short demo of the above code using the image and mask from the question:

    demo