I have a MKAnnotationView that the user can drag around the map.
It is very difficult for a user to drag the pin. I've tried increasing the frame size and also using a giant custom image. But nothing seems to actually change the hit area for the drag to be larger than default.
Consequently, I have to attempt to tap/drag about ten times before anything happens.
MKAnnotationView *annView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"bluedot"] autorelease];
UIImage *image = [UIImage imageNamed:#"blue_dot.png"];
annView.image = image;
annView.draggable = YES;
annView.selected = YES;
return annView;
What am I missing here?
EDIT:
It turns out the problem is that MKAnnotationView needs to be touched before you can drag it. I was having trouble because there are a lot of pins nearby and my MKAnnotationView is very small.
I didn't realise MKAnnotationView needed to be touched before you can drag it.
To get around this, I created a timer that selected that MKAnnotationView regularly.
NSTimer *selectAnnotationTimer = [[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:#selector(selectCenterAnnotation) userInfo:nil repeats:YES] retain];
and the method it calls:
- (void)selectCenterAnnotation {
[mapView selectAnnotation:centerAnnotation animated:NO];
}
Instead of using a timer, you could select the annotation on mouse down. This way you don't mess with the annotation selection, and don't have a timer for each annotation running all the time.
I had the same problem when developing a mac application, and after selecting the annotation on mouse down, the drag works great.
Just came here to say this still helped me today, multiple years later.
In my application, the user can place a bounding area down with annotations and (hopefully) move them around freely. This turned out to be a bit of a nightmare with this "select first and then move" behaviour and just plain made it difficult and infuriating.
To solve this, I default annotation views to selected
func mapView(_ mapView: MKMapView,
viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let view = MKAnnotationView(annotation: annotation, reuseIdentifier: "annotation")
view.isDraggable = true
view.setSelected(true, animated: false)
return view
}
The problem is that there is an annotation manager that resets all annotations back to deselected, so I get around this in this delegate method
func mapView(_ mapView: MKMapView,
annotationView view: MKAnnotationView,
didChange newState: MKAnnotationView.DragState,
fromOldState oldState: MKAnnotationView.DragState) {
if newState == .ending {
mapView.annotations.forEach({
mapview.view(for: $0)?.setSelected(true, animated: false)
})
}
It's a stupid hack for a stupid problem. It does work though.
Related
How can I implement row-by-row scrolling in an NSTextView object? It has to work regardless of how the user tries to scroll: either if they drag the scroll knob or use a two-finger scroll gesture on the trackpad. Furthermore, the scroll knob should always move in discrete steps. (This is exactly the way Apples terminal emulator works.)
What I tried to do first was to subclass NSTextView and override adjustScroll: (which it inherits from NSView), which is described here, that is:
- (NSRect)adjustScroll:(NSRect)newVisible {
NSRect modifiedVisible = newVisible;
NSFont *font = [self font];
CGFloat lineHeight = [[self layoutManager] defaultLineHeightForFont:font];
modifiedVisible.origin.x = (int)(modifiedVisible.origin.x/lineHeight) * lineHeight;
modifiedVisible.origin.y = (int)(modifiedVisible.origin.y/lineHeight) * lineHeight;
return modifiedVisible;
}
This however only gives row-by-row scrolling when I use the scroll knob. By accident I found out that subclassing NSScrollView would give me row-by-row scrolling also when scrolling with the trackpad, even though it seems I'm not really doing anything (so why this works is a mystery), the only overridden methods are these:
- (instancetype)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
}
The scroll knob, however, still moves continuously. Do I have to subclass NSScroller and override setDoubleValue (I tried doing that, but it didn't work very well so that solution feels difficult, though maybe possible) or can I override reflectScrolledClipView: in my subclass to NSScrollView (that didn't work at all), or are there better solutions?
How do I hide the mapview when I have an overlay on top of the mapview in iOS7? This snippet of code used to work in iOS6 but when i upgrade my app to iOS7 it cease to work.
NSArray *views = [[[self.mapView subviews] objectAtIndex:0] subviews];
[[views objectAtIndex:0] setHidden:YES];
Any suggestions or feedback?
With what incanus said with MKTileOverlay, it is like this in the view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *tileTemplate = #"http://tile.stamen.com/watercolor/{z}/{x}/{y}.jpg";
MKTileOverlay *overlay = [[MKTileOverlay alloc] initWithURLTemplate:tileTemplate];
overlay.canReplaceMapContent = YES;
[self.mapView addOverlay:overlay];
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(37.54827, -121.98857)];
self.mapView.delegate = self;
}
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay
{
MKTileOverlayRenderer *renderer = [[MKTileOverlayRenderer alloc] initWithOverlay:overlay];
return renderer;
}
If you need control over how the overlay feeds the data, you need to subclass MKTileOverlay and override loadTileAtPath:result:
-(void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
NSData *tile = [self someHowGetTileImageIntoNSDataBaseOnPath:path];
if (tile) {
result(tile, nil);
} else {
result(nil, [NSError errorWithDomain: CUSTOM_ERROR_DOMAIN code: 1 userInfo:nil]);
}
}
The MKOverlay protocol requires boundingMapRect:, which should returns MKMapRect for the rectangular region that this overlay covers. However, I personally found that if I override it myself, it voids the prior canReplaceMapContent = YES setting as Apple probably does not like to show a blank gray map. So I just let MKTileMapOverlay handles it instead.
If your overlay is not actually tiles, then MKTileOverlay does not really apply. But I think you probably can fake it but always reporting nil data within loadTileAtPath:result:, and add your real overlay via another overlay. Another option would be just cover the whole world with black polygon overlay, but then the unsuspecting user would possibly be unknowingly streaming more data than he/she likes.
MapKit isn't really designed for direct access to the map view subviews outside of true overlays (e.g. turning off Apple's map underneath).
Two ideas:
Consider using the new iOS 7 MKTileOverlay class along with the canReplaceMapContent property. This has the effect of turning off Apple's underlying map.
Consider a similar but separate library such as the MapBox iOS SDK which can emulate the look of MapKit but has greater flexibility for styling (and also supports back to iOS 5).
I have no idea why you would want to do it but instead of counting the number of subviews, you should just ask the mapView for the number of overlays it has
if ([[mapView overlays] count] > 0)
{
....
}
in ios 5 i was able to disable the double tap zoom by just overriding it with a new double tap gesture. But it seems that the double tap gesture is no longer in the gesturerecognizer array that comes with the mkmapview.
NSArray *gestureRecognizers = [_mapView gestureRecognizers];
for (UIGestureRecognizer *recognizer in gestureRecognizers) {
NSLog(#"%#", recognizer);
}
returns nothing in ios 6, where in ios 5 it would return 2 recognizers, one for single tap and one for double tap.
I'd look through the gesture recognizers of MKMapView's subviews. It's probably still there somewhere.
Of course, messing around with another view's GRs is slightly dubious and will likely break the next time Apple changes something about MKMapView...
EDIT: For the benefit of anyone else reading this, please check that it's a UITapGestureRecognizer and that numberOfTapsRequired == 2 and numberOfTouchesRequired == 1.
Also, instead of disabling double-taps on the map entirely, consider adding a double-tap GR on the annotation and then do [mapDoubleTapGR requireGestureRecognizerToFail:annotationDoubleTapGR]. Again, hacky — don't blame me if it breaks on the next OS update!
This worked for me:
[_mapView.subviews[0] addGestureRecognizer:MyDoubleTapOverrider];
Do you want to let the user do anything with the view? If not, it sufficient to set userInteractionEnabled to NO. If so, what specific interactions do you need to allow? Everything but double-tapping? Why disable that one interaction?
The more we know about your use case, the better the answers we can provide.
This works for me:
//INIT the MKMapView
-(id) init{
...
[self getGesturesRecursive:mapView];
...
}
And then let the recursive function loop through the subviews and find the GR:s.
-(void)getGesturesRecursive:(UIView*)v{
NSArray *gestureRecognizers = [v gestureRecognizers];
for (UIGestureRecognizer *recognizer in gestureRecognizers) {
if ([recognizer isKindOfClass:[UITapGestureRecognizer class]]) {
[v removeGestureRecognizer:recognizer];
}
}
for (UIView *v1 in v.subviews){
[self getGesturesRecursive:v1];
}
}
This example removes all tap-GR:s. But I guess you can specify to remove whatever you'd like.
You can use a long tap gesture instead, that works.
I'm working on a custom UITableViewCell subclass, where everything is drawn in code rather than using UILabels etc. (This is part learning exercise and partly because drawing in code is much faster. I know that for a couple of labels it wouldn't make a huge difference, but eventually I'll want to generalise this to more complex cells.)
Currently I'm struggling with the delete button animation: how to animate the cell shrinking as the delete button slides in.
Firstly, I am drawing in a custom subview of the cell's contentView. Everything is drawn in that one subview.
I am setting the subview's size by catching layoutSubviews on the cell itself, and doing:
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect b = [self.contentView bounds];
[subcontentView setFrame:b];
}
I'm doing this rather than just setting an autoresizing mask because it seemed more reliable in testing, but I can use the autoresizing mask approach in testing if needed.
Now, the default thing that happens when someone hits the minus is the view gets squished.
I can avoid that by, when setting up my cell, calling
subcontentView.contentMode = UIViewContentModeRedraw;
That gives me the correct end result (my custom view redraws with the new size, and is laid out properly, like the first image I posted), but the animation of the transition is unpleasant: it looks like the cell stretches and shrinks back to size.
I know why the animation is working like that: Core Animation doesn't ask your view to redraw for each frame, it gets it to redraw for the end position of the animation and then interpolates to find the middle bits.
Another solution is to do
subcontentView.contentMode = UIViewContentModeLeft;
That just draws the delete button over my cell, so it covers part of it.
If I also implement
- (void) didTransitionToState:(UITableViewCellStateMask)state
{
[self setNeedsDisplay];
}
then once the delete button has slid in the cell 'jumps' to the correct size. That way there's no nice slidey animation, but at least I get the correct result at the end.
I guess I could run my own animation in parallel with the delete button appearing, temporarily creating another view with a copy of the image of my view in the old size, setting mine to the new size, and fading between them — that way there would be a nice cross fade instead of a sharp jump. Anyone use such a technique?
Now, you might ask why I can't use the contentStretch property and give it a region to resize. The problem with that is I'm making something to be reasonably generic, so it's not always going to be possible. In this particular example it'd work, but a more complex cell may not.
So, my question (after all of this background) is: what do you do in this situation? Does anyone have the animating delete button working for custom drawn cells? If not, what's the best compromise?
This worked for me finally. in subclass of UITableViewCell
subDrawContentView.contentMode = UIViewContentModeLeft;
overide layout subviews method
- (void)layoutSubviews {
CGRect b = [subDrawContentView bounds];
b.size.width = (!self.showingDeleteConfirmation) ? 320 : 300;
[subDrawContentView setFrame:b];
[subDrawContentView setNeedsDisplay];
[super layoutSubviews];
}
So I will paste the code first and then I will explain:
-(void)startCancelAnimation{
[cancelButton setAlpha:0.0f];
[cancelButton setFrame:CGRectMake(320., cancelButton.frame.origin.y, cancelButton.frame.size.width, cancelButton.frame.size.height)];
cancelButton.hidden=NO;
[UIView animateWithDuration:0.4
animations:^(void){
[progressView setFrame:CGRectMake(progressView.frame.origin.x, progressView.frame.origin.y, 159.0, progressView.frame.size.height)];
[text setFrame:CGRectMake(text.frame.origin.x, text.frame.origin.y, 159.0, text.frame.size.height)];
[cancelButton setFrame:CGRectMake(244., cancelButton.frame.origin.y, cancelButton.frame.size.width, cancelButton.frame.size.height)];
[cancelButton setAlpha:1.0f];
} ];
}
-(void)stopCancelAnimation{
[UIView animateWithDuration:0.4
animations:^(void){
[cancelButton setFrame:CGRectMake(320., cancelButton.frame.origin.y, cancelButton.frame.size.width, cancelButton.frame.size.height)];
[cancelButton setAlpha:0.0f];
}completion:^(BOOL completion){
cancelButton.hidden=YES;
[cancelButton setAlpha:1.0f];
[progressView setFrame:CGRectMake(progressView.frame.origin.x, progressView.frame.origin.y, DEFAULT_WIDTH_PROGRESS, progressView.frame.size.height)];
[text setFrame:CGRectMake(text.frame.origin.x, text.frame.origin.y, DEFAULT_WIDTH_TEXT, text.frame.size.height)];
}
];
}
-(void)decideAnimation{
if([cancelButton isHidden]){
[self startCancelAnimation];
}
else{
[self stopCancelAnimation];
}
}
So what I have there is a button that looks like this:
I have an IBOutlet to it. And what I am doing is resizing a UIProgressView and a UITextField (you can resize whatever you want). As for the code is pretty simple, but if you need any help to understand what's going on, please ask. Also, don't forget to add the Swip Gesture to the UITableView... Like this:
UISwipeGestureRecognizer *gesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(didSwipe:)];
gesture.numberOfTouchesRequired=1;
gesture.direction = UISwipeGestureRecognizerDirectionRight;
[table addGestureRecognizer:gesture];
[gesture release];
Finally the method that does it all:
-(void)didSwipe:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//Get the cell of the swipe...
CGPoint swipeLocation = [gestureRecognizer locationInView:self.table];
NSIndexPath *swipedIndexPath = [self.table indexPathForRowAtPoint:swipeLocation];
UITableViewCell* swipedCell = [self.table cellForRowAtIndexPath:swipedIndexPath];
//Make sure its a cell with content and not an empty one.
if([swipedCell isKindOfClass:[AMUploadsTableViewCell class]]){
// It will start the animation here
[(AMUploadsTableViewCell*)swipedCell decideAnimation];
// Do what you want :)
}
}
}
So as you can see the whole animation is created manually, so you can control exactly what you want. :)
I've noticed this functionality in the iPhone Mail.app and SMS.app apps, but I'm unsure how to implement it myself.
In a standard UITableView, when a user taps the 'Edit' button and the deletion buttons move into position, as the content views are sliding to the right, they perform a quick fade transition where they are replaced by thinner versions of themselves (to account for the space the deletion button is taking up).
I initially thought this was done by calling the following method when editing mode is enabled:
[self.tableView reloadRowsAtIndexPaths: [tableView indexPathsForVisibleRows] withRowAnimation: UITableViewRowAnimationFade];
However this cannot be right as it also fade transitions the delete button itself as well as any accessory views the cell has (suffice it to say, it looks quite odd).
I was wondering, how would it be possible to force the content view to redraw itself, and to then fade the new drawn version over the old one when the table enters edit mode?
UPDATE:
Thanks to Caleb's answer, this is the final block of code that allowed me to get what I was after:
My final solution was to subclass UITableViewCell and then apply the following code to the setEditing accessor:
-(void) setEditing: (BOOL)editing animated: (BOOL)animated
{
[super setEditing: editing animated: animated];
CATransition *animation = [CATransition animation];
animation.duration = 0.2f;
animation.type = kCATransitionFade;
//redraw the subviews (and animate)
for( UIView *subview in self.contentView.subviews )
{
[subview.layer addAnimation: animation forKey: #"editingFade"];
[subview setNeedsDisplay];
}
}
I probably should just clarify as well. Since performance is of utmost importance, everything inside my cell's contentView is being rendered through CG (ie drawRect:), so I can't control any elements being drawn in there specifically.
That animation happens when you call -setEditing:animated: on the table. The table then calls -setEditing:animated: on each of the visible cells. It looks like the standard behavior for a table cell is to adjust the size of the content views, shift them right, and remove the right accessory. If you have your own cells and want a different look in editing mode, I think you can override -setEditing:animated: to adjust the cell content as you like.