Recognizing Touch Events Only Inside the Masked View

Recognizing touch events only inside the masked view

If you want to do this perfectly, use the UIGestureRecognizerDelegate method gestureRecognizer(gesture, shouldReceiveTouch: touch) -> Bool. You will need to map the given gesture recogniser to a particular hexagon and then do pixel precise hit-testing on the image for that hexagon. This latter part is achieved by rendering the mask image to a graphics context and finding the pixel at the point corresponding to the touch location.

However, this is likely overkill. You can simplify the problem by hit-testing each shape as a circle, not a hexagon. The circle shape roughly approximates the hexagon so it will work almost the same for a user and avoids messy pixel-level alpha equality. The inaccuracy of touch input will cover up the inaccurate regions.

Another option is to rework your views to be based on CAShapeLayer masks. CAShapeLayer includes a path property. Bezier paths in UIKit include their own rolled versions of path-contains-point methods so you can just use that for this purpose.

Why doesn't my UILabel in a nested view receive touch events / How can I test the Responder Chain?

It looks like the main reason you're not getting a tap recognized is because you are adding a UILabel as a subview of a UIView, but you're not giving that UIView any constraints. So the view ends up with a width and height of Zero, and the label exists outside the bounds of the view.

Without seeing all of your code, it doesn't look like you need the extra view holding the label.

Take a look at this... it will add a vertical stack view to the main view - centered X and Y - and add "colorChoice" labels to the stack view:

class TestViewController: UIViewController {

let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 4
return v
}()

var colorLabelDict: [Int: UIView] = [:]


override func viewDidLoad() {
super.viewDidLoad()

let v1 = makeColorLabel(colorName: "red", bgColor: .red, fgColor: .white)
let v2 = makeColorLabel(colorName: "green", bgColor: .green, fgColor: .black)
let v3 = makeColorLabel(colorName: "blue", bgColor: .blue, fgColor: .white)

[v1, v2, v3].forEach {
stack.addArrangedSubview($0)
}

stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)

NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

func makeColorLabel(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UILabel {
let colorNumber:Int = colorLabelDict.count
// create tap gesture recognizer
let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
let colorChoice: UILabel = {
let l = UILabel()
l.tag = 700 + colorNumber
l.addGestureRecognizer(tapColorGR)
l.text = colorName
l.textAlignment = .center
l.textColor = fgColor
l.backgroundColor = bgColor
l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
l.layer.borderColor = fgColor.cgColor
l.layer.borderWidth = 1
l.layer.cornerRadius = 20
l.layer.masksToBounds = true
l.adjustsFontSizeToFitWidth = true
l.translatesAutoresizingMaskIntoConstraints = false
// default .isUserInteractionEnabled for UILabel is false, so enable it
l.isUserInteractionEnabled = true
return l
}()
NSLayoutConstraint.activate([
// label height: 50, width: 100
colorChoice.heightAnchor.constraint(equalToConstant: 50),
colorChoice.widthAnchor.constraint(equalToConstant: 100),
])
// assign reference to this label in colorLabelDict dictionary
colorLabelDict[colorNumber] = colorChoice
// return newly created label
return colorChoice
}

@objc func tapColor(sender:UITapGestureRecognizer) {
print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
// unwrap the view that was tapped, make sure it's a UILabel
guard let tappedView = sender.view as? UILabel else {
return
}
let cn = tappedView.tag
let colorNumber = cn
print("The \(tappedView.text ?? "No text") label was tapped.")
}
}

Result of running that:

Sample Image

Those are 3 UILabels, and tapping each will trigger the tapColor() func, printing this to the debug console:

A Color was tapped...with tag:700
The red label was tapped.
A Color was tapped...with tag:701
The green label was tapped.
A Color was tapped...with tag:702
The blue label was tapped.

Detect touches only on non-transparent pixels of UIImageView, efficiently

Here's my quick implementation: (based on Retrieving a pixel alpha value for a UIImage)

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
//Using code from https://stackoverflow.com/questions/1042830/retrieving-a-pixel-alpha-value-for-a-uiimage

unsigned char pixel[1] = {0};
CGContextRef context = CGBitmapContextCreate(pixel,
1, 1, 8, 1, NULL,
kCGImageAlphaOnly);
UIGraphicsPushContext(context);
[image drawAtPoint:CGPointMake(-point.x, -point.y)];
UIGraphicsPopContext();
CGContextRelease(context);
CGFloat alpha = pixel[0]/255.0f;
BOOL transparent = alpha < 0.01f;

return !transparent;
}

This assumes that the image is in the same coordinate space as the point. If scaling goes on, you may have to convert the point before checking the pixel data.

Appears to work pretty quickly to me. I was measuring approx. 0.1-0.4 ms for this method call. It doesn't do the interior space, and is probably not optimal.

How can I click a button behind a transparent UIView?

Create a custom view for your container and override the pointInside: message to return false when the point isn't within an eligible child view, like this:

Swift:

class PassThroughView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for subview in subviews {
if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
return true
}
}
return false
}
}

Objective C:

@interface PassthroughView : UIView
@end

@implementation PassthroughView
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *view in self.subviews) {
if (!view.hidden && view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event])
return YES;
}
return NO;
}
@end

Using this view as a container will allow any of its children to receive touches but the view itself will be transparent to events.

MKMapView not responding to touch events or gesture recognizers

AutoResizingMask is the property by by the UI Elements (eg. Label) shift when the boundaries change. Means when orientation is change the app adapts and does not crash. But if you will set autoResizingMask to Constraint, you are telling it to change constraints as per the autoResizingMask. This is the default behaviour of every UI Element. This helps us to quickly and dynamically arrange UI Elements with methods like self.view.centre or self.view.frame or self.label.size.width etc.

Setting translatesAutoresizingMaskIntoConstraints = NO you are actually removing this behaviour. Therefore you must provide the UI Element fixed co-ordinates, so that the UI Element stays there no matter the orientation of screen. But you are not doing so. So the UIElement has no constraints or a fixed location and behave erractically.

You must not set it to NO, if you don't want to play dynamically with UI's location or size. Like input based growing textfield or some crude animation.

Here is Apple trying to tell you the same thing.

When a view’s bounds change, that view automatically resizes its
subviews according to each subview’s autoresizing mask. You specify
the value of this mask by combining the constants described in
UIViewAutoresizing using the C bitwise OR operator. Combining these
constants lets you specify which dimensions of the view should grow or
shrink relative to the superview. The default value of this property
is UIViewAutoresizingNone, which indicates that the view should not be
resized at all.

When more than one option along the same axis is set, the default
behavior is to distribute the size difference proportionally among the
flexible portions. The larger the flexible portion, relative to the
other flexible portions, the more it is likely to grow. For example,
suppose this property includes the UIViewAutoresizingFlexibleWidth and
UIViewAutoresizingFlexibleRightMargin constants but does not include
the UIViewAutoresizingFlexibleLeftMargin constant, thus indicating
that the width of the view’s left margin is fixed but that the view’s
width and right margin may change. Thus, the view appears anchored to
the left side of its superview while both the view width and the gap
to the right of the view increase.

If the autoresizing behaviors do not offer the precise layout that you
need for your views, you can use a custom container view and override
its layoutSubviews method to position your subviews more precisely.

I do such mistakes a million times , this blog has a nice solution to it.

https://www.innoq.com/en/blog/ios-auto-layout-problem/

Detecting tap inside a bezier path

There is a function CGPathContainsPoint() it may be useful in your case.

Also be careful if you get gesture point from superview, the coordinate may not be correct with your test. You have a method to convertPoint from or to a particular view's coordinate system:

- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view

UIControl Not Receiving Touches

tl;dr Set all subviews of the UIControl to setUserInteractionEnabled:NO. UIImageViews have it set to NO by default.

Original Post

One thing I found recently is that it helps if the top-most subview of the UIControl has setUserInteractionEnabled:NO. I arrived at this because I had a UIControl subclass with a UIImageView as it's only subview and it worked fine. UIImageView has userInteractionEnabled set to NO by default.

I also had another UIControl with a UIView as it's top most subview (technically the same UIControl in a different state). I believe UIView defaults to userInteractionEnabled == YES, which precluded the events being handled by the UIControl. Settings the UIView's userInteractionEnabled to NO solved my issue.

I don't know if it's the same issue here, but maybe that will help?

--
Edit: When I say topmost view... probably set all subviews of the UIControl to setUserInteractionEnabled:NO



Related Topics



Leave a reply



Submit