Cristian
Cristian

Reputation: 495

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

Upvotes: 0

Views: 3005

Answers (1)

Stephan Schlecht
Stephan Schlecht

Reputation: 27126

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

Upvotes: 4

Related Questions