How can I work around this MapKit bug that causes duplicate callouts? - cocoa

There's a bug in MapKit that can cause duplicate callout views on an annotation. If the timing is just right, an annotation view can get re-used while it is being selected and apparently just before the callout view is actually added to it. As a result, the old callout view gets stuck there, and the new callout will appear on top of or next to it. Here's what this can look like in an OS X app:
There's only one annotation on this map. If you click elsewhere on the map to deselect the annotation, only one of the callouts disappears. In some cases you might have two callouts with completely different information, which is where things get really confusing for someone using your app.
Here's the majority of a sample OS X project I put together that illustrates this bug:
#import MapKit;
#import "AppDelegate.h"
#import "JUNMapAnnotation.h"
#interface AppDelegate () <MKMapViewDelegate>
#property (weak) IBOutlet NSWindow *window;
#property (weak) IBOutlet MKMapView *mapView;
#property BOOL firstPin;
- (void)placeAndSelectPin;
- (JUNMapAnnotation *)placePin;
- (void)clearPins;
#end
#implementation AppDelegate
- (IBAction)dropSomePins:(id)sender {
self.firstPin = YES;
[self placeAndSelectPin];
[self performSelector:#selector(placeAndSelectPin) withObject:nil afterDelay:0.0001];
}
#pragma mark - Private methods
- (void)placeAndSelectPin {
[self clearPins];
JUNMapAnnotation *annotation = [self placePin];
[self.mapView deselectAnnotation:annotation animated:NO];
[self.mapView selectAnnotation:annotation animated:YES];
}
- (JUNMapAnnotation *)placePin {
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(50.0,50.0);
JUNMapAnnotation *annotation = [[JUNMapAnnotation alloc] initWithCoordinate:coord];
annotation.title = #"Annotation";
annotation.subtitle = (self.firstPin) ? #"This is an annotation with a longer subtitle" : #"This is an annotation";
[self.mapView addAnnotation:annotation];
self.firstPin = NO;
return annotation;
}
- (void)clearPins {
[self.mapView removeAnnotations:self.mapView.annotations];
}
#pragma mark - MKMapViewDelegate
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
if ([annotation isKindOfClass:[JUNMapAnnotation class]]) {
static NSString *identifier = #"annotationView";
MKPinAnnotationView *view = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (view == nil) {
view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
view.canShowCallout = YES;
NSLog(#"new annotation view");
} else {
view.annotation = annotation;
}
return view;
}
return nil;
}
#end
The same bug seems to exist in iOS, though I've had a tougher time recreating it there.
While I'm waiting on Apple to fix this, I'd like to work around it as much as possible. So far I've come up with a few possibilities:
Don't re-use annotation views. From what I can tell this seems like the only way to completely avoid the bug, but it seems pretty inefficient.
When an annotation view is re-used in mapView:viewForAnnotation:, remove all of its subviews. Currently it seems like the callout is the only subview, though it doesn't seem like a particularly safe hack. It also only sort of works—it doesn't prevent duplicate callouts from appearing, it just keeps them from sticking around forever. (When this bug first happens, there actually aren't any subviews yet.)
Combine both of those: if dequeueReusableAnnotationViewWithIdentifier: returns a view that has any subviews, ignore it and create a new one. This seems a lot safer than 2 and isn't nearly as inefficient as 1. But as with 2 it's not a complete workaround.
I've also tried adding deselectAnnotation:animated: in every place I can think of, but I can't find anything that works. I assume that once the annotation view is re-used, the MapView loses track of the first callout, so none of its normal methods will get rid of it.

this is a bit out of left field, but..
try registering the same cell class with 2 different reuse identifiers. in viewForAnnotation:, alternate between using each identifier when dequeueing a cell. this should prevent grabbing from the same queue twice in succession.

Related

Detecting UIScrollView Motion

My UIScrollView is moving using this code:
[self.scrollView setContentOffset:newOffset animated:YES];
And it will trigger
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
Multiple times, and that OK. However, I am seeking a way to query the scroll view and know whether it has reached its' final station, or it is still in motion. Is there a way to do that? isTracking and decelerating are returning NO.
Thank you..!
Make sure your controller conforms to <UIScrollViewDelegate>...
and implement:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
From Apple's docs:
Discussion
The scroll view calls this method at the end of its implementations of the setContentOffset(_:animated:) and scrollRectToVisible(_:animated:) methods, but only if animations are requested.
add a BOOL property:
#property (assign, readwrite) BOOL bIsScrolling;
then you can do this:
self.bIsScrolling = YES;
[self.scrollView setContentOffset:newOffset animated:YES];
and this:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
NSLog(#"done scrolling");
self.bIsScrolling = NO;
}
and elsewhere you can check if (bIsScrolling) ...

MapKit changes annotation view order when map is scrolled in XCode 4.6

I am an experienced iOS developer, but this has really stumped me. I am simultaneously submitting a problem report to Apple.
I'm adding annotations to a MKMapKit map (Xcode 4.6). Each annotation is a MyAnnotation class; MyAnnotation defines a property, location_id, that I use to keep track of that annotation.
The problem is simple: I want the MyAnnotation with a location_id of -1 to appear in front of everything else.
To do this, I am overriding mapView:didAddAnnotationViews: in my MKMapViewDelegate:
-(void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
// Loop through any newly added views, arranging them (z-index)
for (MKAnnotationView* view in views) {
// Check the location ID
if([view.annotation isKindOfClass:[MyAnnotation class]] && [((MyAnnotation*)(view.annotation)).location_id intValue]==-1 ) {
// -1: Bring to front
[[view superview] bringSubviewToFront:view];
NSLog(#"to FRONT: %#",((MyAnnotation*)view.annotation).location_id);
} else {
// Something else: send to back
[[view superview] sendSubviewToBack:view];
NSLog(#"to BACK: %#",((MyAnnotation*)view.annotation).location_id);
}
}
}
this works just fine. I have an "add" button that adds an annotation to a random location near the center of my map. Each time I push the "add" button, a new annotation appears; but nothing hides the annotation with the location_id of -1.
** UNTIL ** I scroll!
As soon as I start scrolling, all of my annotations are rearranged (z-order) and my careful stacking no longer applies.
The really confusing thing is, I've done icon order stacking before with no problem whatsoever. I've created a brand new, single view app to test this problem out; sending MKAnnotationView items to the back or front only works until you scroll. There is nothing else in this skeleton app except the code described above. I'm wondering if there is some kind of bug in the latest MapKit framework.
The original problem was in trying to add new annotations when the user scrolled (mapView:regionDidChangeAnimated:). The annotation adds; the mapView:didAddAnnotationViews: code fires; and then the order is scrambled by an unseen hand in the framework (presumably as the scroll completes).
In case you're interested, here is my viewForAnnotation:
-(MKAnnotationView*)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
// Is this an A91Location?
if([annotation isKindOfClass:[MyAnnotation class]]){
MyAnnotation* ann=(MyAnnotation*)annotation;
NSLog(#"viewForAnnotation with A91Location ID %#",ann.location_id);
if([ann.location_id intValue]==-1){
// If the ID is -1, use a green pin
MKPinAnnotationView* green_pin=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:nil];
green_pin.pinColor=MKPinAnnotationColorGreen;
green_pin.enabled=NO;
return green_pin;
} else {
// Otherwise, use a default (red) pin
MKPinAnnotationView* red_pin=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:nil];
red_pin.enabled=NO;
return red_pin;
}
}
// Everything else
return nil;
}
And my class:
#interface MyAnnotation : NSObject <MKAnnotation>
#property (strong, nonatomic, readonly) NSNumber* location_id;
#property (strong, nonatomic, readonly) NSString* name;
#property (strong, nonatomic, readonly) NSString* description;
#property (nonatomic) CLLocationCoordinate2D coordinate;
-(id) initWithID:(NSNumber*)location_id name: (NSString*) name description:(NSString*) description location:(CLLocationCoordinate2D) location;
// For MKAnnotation protocol... return name and description, respectively
-(NSString*)title;
-(NSString*)subtitle;
#end
Could you possibly try the following as a work around:
1) Remove your code in mapView:didAddAnnotationViews:
2) Pickup when the map is moved or pinched (I assume this is what you consider to be a scroll right?) - I do this with gestures rather than the mapView regionChanged as I have had bad experiences such as unexplained behaviour with the latter.
//recognise the paning gesture to fire the didDragMap method
UIPanGestureRecognizer* panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(didDragMap:)];
[panRec setDelegate:self];
[self.mapView addGestureRecognizer:panRec];
//recognise the pinching gesture to fire the didPinchMap method
UIPinchGestureRecognizer *pinchRec = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:#selector(didPinchMap:)];
[pinchRec setDelegate:self];
//[pinchRec setDelaysTouchesBegan:YES];
[self.mapView addGestureRecognizer:pinchRec];
//recognise the doubleTap
UITapGestureRecognizer *doubleTapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didPinchMap:)];
[doubleTapRec setDelegate:self];
doubleTapRec.numberOfTapsRequired = 2;
[self.mapView addGestureRecognizer:doubleTapRec];
3) write a custom "plotAnnotations" function to show your annotations. This function will loop through all your annotations and save them in an array "annotationsToShow" ordered by your location id DESC so that your location_id -1 are added last. Use [self.mapView addAnnotations:annotationsToShow]; to display them.
4) Call plotAnnotations in your gestureRecognizer functions
- (void)didDragMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self plotAnnotations];
}
}
- (void)didPinchMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self plotAnnotations];
}
}
5) You may need to delete all annotations before displaying the new ones in 3) [self.mapView removeAnnotations:annotationsToRemove];

How to change color of divider in NSSplitView?

Can we change the color of the divider?
Apple documentations says, that we can override -dividerColor in subclass of NSSplitView for this, but it doesn't works for me, or my understanding isn't correct. Also I've try create color layer over divider, e.g.:
colorLayer = [CALayer layer];
NSRect dividerFrame = NSMakeRect([[self.subviews objectAtIndex:0] frame].size.width, [[self.subviews objectAtIndex:0] frame].origin.y, [self dividerThickness], self.frame.size.height);
[colorLayer setBackgroundColor:[color coreGraphicsColorWithAlfa:1]];
[colorLayer setFrame:NSRectToCGRect(dividerFrame)];
[self.layer addSublayer:colorLayer];
Not works.
This answer may be late but:
If you are using Interface Builder, it is possible to change the property by going to the Identity Inspector of the NSSplitView (cmd+alt+3) and adding a User Defined Runtime Attribute for dividerColor of the type Color.
Actually, simply subclassing NSSplitView and overriding -(void)dividerColor works, but works only for thin or thick divider.
I've created simple configurable split view like this:
#interface CustomSplitView : NSSplitView
#property NSColor* DividerColor
#end
#implementation CustomSplitView
- (NSColor*)dividerColor {
return (self.DividerColor == nil) ? [super dividerColor] : self.DividerColor;
}
#end
Then in Interface Builder specify custom class for your split view to be CustomSplitView and add new user defined runtime attribute with key path = DividerColor, type = Color and select desired splitter color.
I've tried subclassing - (void)dividerColor too and I'm not sure why it doesn't work even though I know it's being called (and it's in the documentation).
One way to change the color of the divider is to subclass - (void)drawDividerInRect:(NSRect)aRect. However, for some reason, this method isn't called and I've checked all over the web for answers, but couldn't find anything, so I ended up calling it from drawRect. Here is the code for the subclassed NSSplitView:
-(void) drawRect {
id topView = [[self subviews] objectAtIndex:0];
NSRect topViewFrameRect = [topView frame];
[self drawDividerInRect:NSMakeRect(topViewFrameRect.origin.x, topViewFrameRect.size.height, topViewFrameRect.size.width, [self dividerThickness] )];
}
-(void) drawDividerInRect:(NSRect)aRect {
[[NSColor redColor] set];
NSRectFill(aRect);
}
Based on Palle's answer, but with the possibility to change the color dynamically in code, I'm currently using this solution (Swift 4):
splitView.setValue(NSColor.red, forKey: "dividerColor")
If your splitview control is part of a NSSplitViewController, you should use something like this:
splitViewController?.splitView.setValue(NSColor.red, forKey: "dividerColor")
In Swift and on macOS 11 I was able to achieve this by simply subclassing the NSSPlitView and only override drawDivider()
import Foundation
import AppKit
class MainSplitView: NSSplitView {
override func drawDivider(in rect: NSRect) {
NSColor(named: "separatorLinesColor")?.setFill()
rect.fill()
}
}
I had previously tried some of the other way, listed in here and what used to work stopped working with macOS 11... but it seems that this works.
One important point I haven't seen mentioned anywhere is that if you are overriding drawRect in a split view then you must call super -- otherwise drawDividerInRect: is never called. So, it should go something like this:
- (void)drawRect:(NSRect)dirtyRect {
// your other custom drawing
// call super last to draw the divider on top
[super drawRect:dirtyRect];
}
- (void)drawDividerInRect:(NSRect)aRect {
[[NSColor blueColor] set];
NSRectFill(aRect);
}

Pros and cons of using CCUIViewWrapper versus a ported functionality

I'm trying to understand the pros and cons of using something CCUIViewWrapper in Cocos2d versus a ported functionality. For instance, would it be better to use a UITableView in a CCUIViewWrapper, or to use the CCTableViewSuite. At first glance, I would assume the wrapper is the better approach since presumably it allows me to do everything the UITableView offers, but are there key details I'm missing? Are there severe limitations that exist with the wrapper either with actually using the apple sdk object or with not being able to take advantage of certain features within Cocos2d that come with a ported object like CCTableView?
I have copied this post from here that may help answer your question. I believe that it would be better to uses cocos2d functions with cocos2d wrappers but you can implement with others it just doesn't integrate as well.
If there are any newbies (like myself) out there that need extra help how to integrate a UIKit item with cocos2d, this might help. As you have read in the first post of this thread Blue Ether has written a wrapper for manipulating UIViews. What is a UIView? Look at http://developer.apple.com/iphone/library/documentation/uikit/reference/uikit_framework/Introduction/Introduction.html, scroll down a little bit and you will see a diagram of different UIView items; things like UIButton, UISlider, UITextView, UILabel, UIAlertView, UITableView, UIWindow and so on. There are more then 20 of them. You can use Blue Ethas code to wrap any of them inside cocos2d. Here I will wrap a UIButton, but experiment with your favorite choice of UIView item.
Create a new cocos 2d Application project.
Make a new NSObject file and call it CCUIViewWrapper. That will give you the files CCUIViewWrapper.h and CCUIViewWrapper.m. Edit (by copy paste) them so they look as Blue Ether has defined them (see the first post in this thread.
Make your HelloWorld.h look like this
// HelloWorldLayer.h
#import "cocos2d.h"
#import <UIKit/UIKit.h>
#import "CCUIViewWrapper.h"
#interface HelloWorld : CCLayer
{
UIButton *button;
CCUIViewWrapper *wrapper;
}
+(id) scene;
#end
and your HelloWorld.m like this
// HelloWorldLayer.m
#import "HelloWorldScene.h"
// HelloWorld implementation
#implementation HelloWorld
+(id) scene
{
CCScene *scene = [CCScene node];
HelloWorld *layer = [HelloWorld node];
[scene addChild: layer];
return scene;
}
-(void)buttonTapped:(id)sender
{
NSLog(#"buttonTapped");
}
// create and initialize a UIView item with the wrapper
-(void)addUIViewItem
{
// create item programatically
button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button addTarget:self action:#selector(buttonTapped:) forControlEvents:UIControlEventTouchDown];
[button setTitle:#"Touch Me" forState:UIControlStateNormal];
button.frame = CGRectMake(0.0, 0.0, 120.0, 40.0);
// put a wrappar around it
wrapper = [CCUIViewWrapper wrapperForUIView:button];
[self addChild:wrapper];
}
-(id) init
{
if( (self=[super init] )) {
[self addUIViewItem];
// x=160=100+120/2, y=240=260-40/2
wrapper.position = ccp(100,260);
[wrapper runAction:[CCRotateTo actionWithDuration:4.5 angle:180]];
}
return self;
}
- (void) dealloc
{
[self removeChild:wrapper cleanup:true];
wrapper = nil;
[button release];
button=nil;
[super dealloc];
}
#end
Build and Run. This should produce a rotating button in the middle of the scene. You can touch the button while it rotates. It will write a text message in the Console.
CCUIViewWrapper is not a good solution. I have tried and removed it. The problem is that it wraps the UIKit element in a container and adds it as a view on director over the openGL main view. One problem with that is that you will not be able to add any other sprite on top of it. Very limited.

Cocoa - loading a view from a nib and displaying it in a NSView container , as a subview

I've asked about this earlier but the question itself and all the information in it might have been a little confusing, plus the result i want to get is a little more complicated. So i started a new clean test project to handle just the part that im interested to understand for the moment.
So what i want, is basically this: i have a view container (inherits NSView). Inside, i want to place some images, but not just simple NSImage or NSImageView, but some custom view (inherits NSView also), which itself contains a textfield and an NSImageView. This 'image holder' as i called it, is in a separate nib file (im using this approach since i am guiding myself after an Apple SAmple Application, COCOA SLIDES).
The results i got so far, is something but not what i am expecting. Im thinking i must be doing something wrong in the Interface Builder (not connecting the proper thingies), but i hope someone with more expertise will be able to enlighten me.
Below i'll try to put all the code that i have so far:
//ImagesContainer.h
#import <Cocoa/Cocoa.h>
#interface ImagesContainer : NSView {
}
#end
//ImagesContainer.m
#import "ImagesContainer.h"
#import "ImageHolderView.h"
#import "ImageHolderNode.h"
#class ImageHolderView;
#class ImageHolderNode;
#implementation ImagesContainer
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
//create some subviews
for(int i=0;i<3;i++){
ImageHolderNode *node = [[ImageHolderNode alloc] init];
[self addSubview:[node rootView]];
}
}
NSRunAlertPanel(#"subviews", [NSString stringWithFormat:#"%d",[[self subviews] count]], #"OK", NULL, NULL);
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
[[NSColor blackColor] set];
NSRectFill(NSMakeRect(0,0,dirtyRect.size.width,dirtyRect.size.height));
int i=1;
for(NSView *subview in [self subviews]){
[subview setFrameOrigin:NSMakePoint(10*i, 10)];
i++;
}
}
#end
//ImageHolderView.h
#import <Cocoa/Cocoa.h>
#interface ImageHolderView : NSView {
IBOutlet NSImageView *imageView;
}
#end
//ImageHolderVIew.m
#import "ImageHolderView.h"
#implementation ImageHolderView
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
[[NSColor blueColor]set];
NSRectFill(NSMakeRect(10,10, 100, 100));
//[super drawRect:dirtyRect];
}
#end
//ImageHolderNode.h
#import <Cocoa/Cocoa.h>
#class ImageHolderView;
#interface ImageHolderNode : NSObject {
IBOutlet ImageHolderView *rootView;
IBOutlet NSImageView *imageView;
}
-(NSView *)rootView;
-(void)loadUIFromNib;
#end
//ImageHolderNode.m
#import "ImageHolderNode.h"
#implementation ImageHolderNode
-(void)loadUIFromNib {
[NSBundle loadNibNamed:#"ImageHolder" owner: self];
}
-(NSView *)rootView {
if( rootView == nil) {
NSRunAlertPanel(#"Loading nib", #"...", #"OK", NULL, NULL);
[ self loadUIFromNib];
}
return rootView;
}
#end
My nib files are:
MainMenu.xib
ImageHolder.xib
MainMenu is the xib that is generated when i started the new project.
ImageHolder looks something like this:
image link
I'll try to mention the connections so far in the xib ImageHolder :
File's Owner - has class of ImageHolderNode
The main view of the ImageHolder.xib , has the class ImageHolderView
So to resume, the results im getting are 3 blue rectangles in the view container, but i cant seem to make it display the view loaded from the ImageHolder.xib
If anyone wants to have a look at the CocoaSlides sample application , its on apple developer page ( im not allowed unfortunately to post more than 1 links :) )
Not an answer, exactly, as it is unclear what you are asking..
You make a view (class 'ImagesContainer'). Lets call it imagesContainerView.
ImagesContainerView makes 3 Objects (class 'ImageHolderNode'). ImagesContainerView asks each imageHolderNode for it's -rootView (maybe 'ImageHolderView') and adds the return value to it's view-heirarchy.
ImagesContainerView throws away (but leaks) each imageHolderNode.
So the view heirachy looks like:-
+ imagesContainerView
+ imageHolderView1 or maybe nil
+ imageHolderView2 or maybe nil
+ imageHolderView3 or maybe nil
Is this what you are expecting?
So where do you call -(void)loadUIFromNib and wait for the nib to load?
In some code you are not showing?
In general, progress a step at a time, get each step working.
NSAssert is your friend. Try it instead of mis-using alert panels and logging for debugging purposes. ie.
ImageHolderNode *node = [[[ImageHolderNode alloc] init] autorelease];
NSAssert([node rootView], #"Eek! RootView is nil.");
[self addSubview:[node rootView]];
A view of course, should draw something. TextViews draw text and ImageViews draw images. You should subclass NSView if you need to draw something other than text, images, tables, etc. that Cocoa provides.
You should arrange your views as your app requires in the nib or using a viewController or a windowController if you need to assemble views from multiple nibs. Thats what they are for.
EDIT
Interface Builder Connections
If RootView isn't nil then it seems like you have hooked up your connections correctly, but you say you are unclear so..
Make sure the IB window is set to List view so you can see the contents of you nib clearly.
'File's Owner' represents the object that is going to load the nib, right? In your case ImageHolderNode.
Control Click on File's owner and amongst other things you can see it's outlets. Control drag (in the list view) from an outlet to the object you want to be set as the instance var when the nib is loaded by ImageHolderNode. I know you know this already, but there is nothing else to it.
Doh
What exactly are you expecting to see ? An empty imageView? Well, that will look like nothing. An empty textfield? That too, will look like nothing. Hook up an outlet to your textfield and imageView and set some content on them.

Resources