How do I make a MKAnnotationView touch sensitive? - ios

I currently have a map view with some annotation on it. I have the annotation with custom images. The problem I am trying to fix is the sensitivity of the images. When I try to drag them, it feels like I have to touch the exact center for it to be focused on. Is there a way to make the touch bounds bigger?

To do this, you need to subclass MKAnnotationView to create your own custom MKAnnotationView. In your subclass, override the following functions:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
// Return YES if the point is inside an area you want to be touchable
}
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
// Return the deepest view that the point is inside of.
}
This allows interactive views (such as buttons, etc.) to be pressed. The default implementation in MKAnnotationView is not strict on pointInside and hitTest because it allows presses that are actually inside one annotation to get sent to a different annotation. It does this by figuring out the closest annotation center to the touch-point and sending the events to that annotation, this is so that close-together (overlapping) annotations don't block each other from being selected. However, in your case you probably want to block other annotations if the user is to select and drag the topmost annotation, so the above method is probably what you want, or else it will set you on the right path.
EDIT:
I was asked in comments for an example implementation of hitTest:withEvent: - This depends on exactly what you are trying to achieve. The original question suggested touching and dragging images within the annotation whereas in my case I have some buttons inside the annotation that I want to be interactive. Here's my code:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView != nil)
{
// ensure that the callout appears above all other views
[self.superview bringSubviewToFront:self];
// we tapped inside the callout
if (CGRectContainsPoint(self.resultView.callButton.frame, point))
{
hitView = self.resultView.callButton;
}
else if (CGRectContainsPoint(self.resultView.addButton.frame, point))
{
hitView = self.resultView.addButton;
}
else
{
hitView = self.resultView.viewDetailsButton;
}
[self preventSelectionChange];
}
return hitView;
}
As you can see it's quite simple - The MKAnnotationView implementation (called as super on the first line of my implementation) only returns the first (outermost) view, it does not drill down through the view hierarchy to see which sub-view the touch is actually inside. In my case I just check if the touch is inside one of three buttons and return those. In other circumstances you may have simple rectangle-based drilling down through the hierarchy or more complex hit tests for example to avoid transparent areas within your view to allow touches to pass through those parts. If you do need to drill down, CGRectContainsPoint can be used the same way I have used it, but remember to offset your points into local view coordinates for each view-level you drill into.
The preventSelectionChange method is to prevent my custom annotation from becoming selected, I am using it as a customisable/interactive callout from map pins and this keeps the pin it relates to selected instead of allowing the selection to change to this annotation.

Did you subclass MKAnnotationView or did you change the just set the image property of it?
Here's the documentation for setting the image property.
Discussion:
Assigning a new image to this property also changes the size of the view’s frame so that it matches the width and height of the new image. The position of the view’s frame does not change.
Check the size of the frame of your annotation view, which is where your object can receive touches.

I implemented something similar in the following manner
Create a subclass of the Gesture Recognizer class that handles touches
Create a subclass of the UIImage class, this class uses the recognizer class to handle your gestures
Use this in the annotation
The gesture recognizer subclass will handle your gestures if you perform them at any point in the image. This should help you.
Keep us updated on whether this solution worked for u...

#jhabbott solution never worked for me, as I mentioned here.
I have an image and a label side by side. The image was shown by setting annotationview image property, and the label by adding an UILabel
I redirected the func point(inside:with:) method to the UILabel one (which included the image zone) and hitTest did return exactly the same view whether I clicked on the label or the image. But label click did not produce any callback...
Finally, I ended up by enlarging the MKAnnotationView frame to enclose label + image, I set annotationView.image to nil, and I created my custom UIImageView.
Because I wanted the anchor point at the middle of the image, I had to set a custom one :
self.frame = CGRect(x: 0, y: 0, width:
self.myLabel.frame.width, height: self.myLabel.frame.height)
self.centerOffset = CGPoint(x: self.frame.width/2, y: self.myImageView.frame.height/2)
Then I deleted point(inside:with:) and hitTest(point:with:) overrides that did nothing.
And now, for the first time, my annotation view is completely reactive.

Related

UIScrollView scroll over background view

I have a UIScrollView (with a clear background) and behind it I have a UIImage that takes up about 1/3 of the devices height. In order to initial display the image which is sitting being the scroll view I set the scrollviews contentInset to use the same height as the image. This does exactly what I want, initialing showing the image, but scrolling down will eventually cover the image with the scroll views content.
The only issue is I added a button onto of the image. However it cannot be touched because the UIScrollView is actually over the top of it (even though the button can be seen due to the clear background). How can I get this to work.
Edit:
The following solved the problem:
//viewdidload
self.scrollView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "onScrollViewTapped:"))
...
func onScrollViewTapped(recognizer:UITapGestureRecognizer)
{
var point = recognizer.locationInView(self.view)
if CGRectContainsPoint(self.closeButton.frame, point) {
self.closeButton.sendActionsForControlEvents(UIControlEvents.TouchUpInside)
}
}
Thanks for the screenshots and reference to Google maps doing what you're looking for, I can see what you're talking about now.
I noticed that the image is clickable and is scrolled over but there is no button showing on the image itself. What you can do is put a clear button in your UIScrollView that covers the image in order to make it clickable when you're able to see it. You're not going to be able to click anything under a UIScrollView as far as I can tell.
Please let me know if that works for you.
a simple solution is to reorder the views in the document out line. The higher the view in the outline, the lower the view is as a layer
Two things to test:
1) Make sure the image that contains the button has its userInteractionEnabled set to true (the default is false). Although, since the button is a subview and added on top of the ImageView (I assume) then this might not help.
2) If that doesn't help, can you instead add the button as a subview of the UIScrollView and set its position to be where the image is? This way it should stay on the image and will be hidden as the user scrolls down, but clickable since it is a child of the ScrollView.
Some code and/or images would help as well.
I think the way to do this is to subclass whatever objects are in your UIScrollView and override touches began / touches ended. Then figure out which coordinates are being touched and whether they land within the bounds of your button
e.g. in Swift this would be:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent?) {
println("!!! touchesBegan")
if var touch = touches.first {
var touchObj:UITouch = touch as! UITouch
println("touchesBegan \(touchObj.locationInView(self))") //this locationInView should probably target the main screen view and then test coordinates against your button bounds
}
super.touchesBegan(touches, withEvent:event!)
}
See :
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIResponder_Class/index.html#//apple_ref/occ/instm/UIResponder/touchesBegan:withEvent:
And:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITouch_Class/index.html#//apple_ref/occ/instm/UITouch/locationInView:
You should subclass UIScrollView and override -hitTest:withEvent: like so, to make sure it only eats touches it should.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *const inherited = [super hitTest:point withEvent:event];
if (inherited == self) return nil;
return inherited;
}
Also make sure to set userInteractionEnabled to YES in your image view.
There is 2 way you can checked that weather touch event is fire on UIButton or not?
Option 1 : You need to add UITapGesture on UIScrollView. while tapping on UIScrollView. Tap gesture return touch point with respect to UIScrollView. you need to convert that touch point with respect to main UIView(that is self.view) using following method.
CGPoint originInSuperview = [superview convertPoint:CGPointZero fromView:subview];
after successfully conversation, you can checked that weather touch point is interact with UIButton frame or what. if it interact then you can perform you action that you are going to perform on UIButton selector.
CGRectContainsPoint(buttonView.frame, point)
Option 2 : Received first touch event while user touch on iPhone screen. and redirect touch point to current UIViewController. where you can check interact as like in option 1 describe. and perform your action.
Option 2 is already integrated in one of my project successfully but i have forgot the library that received first tap event and redirect to current controller. when i know its name i will remind you.
May this help you.

Make UIView area unclickable

So i have a UIView setup on the Ipad for a small project i am working on. I will be displaying an image or a view on that page. I wanted to know if is it possible to create an invisible border (say 1") around the view that will be unclickable ?
I was thinking of adding a button and disabling it, but i think that would not allow the image to be shown full screen.
I have already setup a recognizer, because i want a three finger swipe to go to the next image. What would be the best approach for this ?
Use a custom UIView class and override hitTest:withEvent:.
- UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGRect frame = CGRectInset(self.bounds, 25, 25);
return CGRectContainsPoint(frame, point) ? self : nil;
}
Adjust the inset to meet your needs.

How can I set up gesture recognizer to interact any UIView when all the views are being animated?

I found the code listed below from https://github.com/DuncanMC/iOS-CAAnimation-group-demo. This particular method allows the user to stop a UIView while it is "in-flight" in a core animation sequence using a gesture recognizer. When the view is tapped, the animation stops. As shown this code will only work on animated view. I have many animated views and I need interaction with any of the views. I think I must set up an array of views (or layers) and cycle through them. Is this correct? How could I do this? Thanks!
/*
This method gets called from a tap gesture recognizer installed on the view myContainerView.
We get the coordinates of the tap from the gesture recognizer and use it to hit-test
myContainerView.layer.presentationLayer to see if the user tapped on the moving image view's
(presentation) layer. The presentation layer's properties are updated as the animation runs, so hit-testing
the presentation layer lets you do tap and/or collision tests on the "in flight" animation.
*/
- (IBAction)testViewTapped:(id)sender
{
CALayer *tappedLayer;
id layerDelegate;
UITapGestureRecognizer *theTapper = (UITapGestureRecognizer *)sender;
CGPoint touchPoint = [theTapper locationInView: myContainerView];
if (animationInFlight)
{
tappedLayer = [myContainerView.layer.presentationLayer hitTest: touchPoint];
layerDelegate = [tappedLayer delegate];
if (((layerDelegate == imageOne && !doingMaskAnimation)) ||
(layerDelegate == waretoLogoLarge && doingMaskAnimation))
{
if (myContainerView.layer.speed == 0)
[self resumeLayer: myContainerView.layer];
else
{
[self pauseLayer: myContainerView.layer];
//Also kill all the pending label changes that we set up using performSelector:withObject:afterDelay
[NSObject cancelPreviousPerformRequestsWithTarget: animationStepLabel];
}
}
}
}
LOL. That demo project is mine. The code is written to find the layer that was tapped, and then use the fact that for a layer that backs a UIView, the layer's delegate is the view itself.
At the point in the code where it finds the layerDelegate, you should make sure it isKindOfClass UIView, then use whatever method is appropriate to match your view with the views you've animated. You could use an IBOutletCollection to keep track of the views that you are animating, or manually create a mutable array and add view objects to it, use view tags, or whatever makes sense for your application.

touches methods not getting called on UIView placed inside a UIScrollView

I have a Custom Scroll View, subclassing UIScrollView. I have added a scroll view in my viewcontroller nib file and changed its class to CustomScrollView. Now, this custom scroll view (made from xib) is added as a subview on self.view.
In this scroll view, I have 3 text fields and 1 UIImageView(named signImageView) added from xib. On clicking UIImageView (added a TapGestureRecogniser), a UIView named signView is added on the custom scroll view. I want to allow User to sign on this view, So I have created a class Signature.m and .h, subclassing UIView and implemented the touches methods (touchesBegan, touchesMoved and touchesEnded) and initialised the signView as follows:
signView = [[Signature alloc]initWithFrame:signImageView.frame];
[customScrollView addSubview:signView];
But when I start signing on the signView, the view gets scrolled and hence the touches methods don't get called.
I have tried adding signView on self.view instead of custom scroll view, but in that case the view remains glued to a fixed position when I start scrolling. (Its frame remains fixed in this case)
Try setting canCancelContentTouches of the scrollView to NO and delaysContentTouches to YES.
EDIT:
I see that similiar question was answered here Drag & sweep with Cocoa on iPhone (the answer is exactly the same).
If the user tap-n-holds the signView (for about 0.3-0.5 seconds) then view's touchesBegan: method gets fired and all events from that moment on go to the signView until touchesEnded: is called.
If user quickly swipes trough the signView then UIScrollView takes over.
Since you already have UIView subclassed with touchesBegan: method implemented maybe you could somehow indicate to user that your app is prepared for him to sign ('green light' equivalent).
You could also use touchesEnded: to turn off this green light.
It might be better if you add signImageView as as subView of signView (instead of to customScrollView) and hide it when touchesBegan: is fired). You would add signView to customScrollview at the same place where you add signImageView in existing code instead.
With this you achieve that there is effectively only one subView on that place (for better touch-passing efficiency. And you could achieve that green light effect by un-hiding signImageView in touchesBegan:/touchesEnded:
If this app-behaviour (0.3-0.5s delay) is unacceptable then you'd also need to subclass UIScrollView. There Vignesh's method of overriding UIScrollView's touchesShouldBegin: could come to the rescue. There you could possibly detect if the touch accoured in signView and pass it to that view immediately.
When ever you add a scrollview in your view hierarchy it swallows all touches.Hence you are not getting the touches began. So to get the touches in your signon view you will have to pass the touches to signon view. This is how you do it.
We achieved this with a UIScrollView subclass that disables the pan gesture recogniser for a list of views that you provide.
class PanGestureSelectiveScrollView: UIScrollView {
var disablePanOnViews: [UIView]?
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let disablePanOnViews = disablePanOnViews else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
let touchPoint = gestureRecognizer.location(in: self)
let isTouchingAnyDisablingView = disablePanOnViews.first { $0.frame.contains(touchPoint) } != nil
if gestureRecognizer === panGestureRecognizer && isTouchingAnyDisablingView {
return false
}
return true
}
}

Dragging uiimageview into a "trashcan" while zoomed in on a uiscrollview

Hey everyone. I've searched the web and really haven't found a problem quite like this. My set up is as follows: I am trying to make an app where a user drags uiimageviewss on a uiview inside a uiscrollview (which can zoom in), but also drag the image to a trashcan to be deleted. When the scrollview is zoomed in, the user can still drag because I have subclassed uiimageview and overridden the touchesMoved method. I want the user to be able to drag the uiimageviews to a trashcan in the bottom right corner of the screen. What I implemented already works when the uiscrollview is normal zoom scale, but when you zoom in you cannot drag the image to the trashcan, because well, the trashcan is a subview of the controller's view itself (this was so that it wouldn't zoom with the uiview) and of course is not in the right coordinates of the screen for the uiscrollview.
I've found a view method called ConvertRect that converts a rectangle to the coordinates of whatever view you give to the method, but this doesn't seem to be working. I do a check within each mapimage(the imageview subclass) that checks the frame of the uiimageview intersects the frame of the trashcan. So I try this method before I do that check, but like I said it doesn't seem to be working.
So with all that complicatedness, let me say in a nutshell my problem. I want to drag images over to a trashcan regardless of what zoom level at uiscrollview is at. I can make the trashcan part of the uiscrollview but I need a way to make it keep in the bottom right corner and also not zoom when the uiscrollview does. Any ideas would be so greatly appreciated, thanks guys. :)
I had the exact same challenge.
In can just describe it in short:
When you start dragging the image REMOVE it from the UISCrollView and add it as a subview the the main view of your ViewController.
make sure that the image won't get lost in the process (means: someone still retaining it).
While dragging, constantly check to see if your image entered or left the boundaries of the trash can. you can do it with the method bellow:
-(void) checkForTrashCan
{
CGRect rect = [self.delegate.trashCan convertRect:self.view.frame fromView:self.view.superview];
if ([self.delegate.trashCan rectIntersectBounds:rect])
{
if (NOT self.isInTrashMode)
{
[self enterTrashMode];
}
}
else
{
if (self.isInTrashMode)
{
[self exitTrashMode];
}
}
}
When the method rectIntersectBounds is just a simple extension method for UIView class that i wrote:
-(BOOL) rectIntersectBounds : (CGRect) rect
{
return CGRectIntersectsRect(self.bounds, rect);
}
When the drag ended check if your view is inside the trash can, and act according to the result.

Resources