Change MKAnnotationView programmatically - objective-c

Is there a way to change MKAnnotationView style (like from red label with number to green colored label with number).
I want to change this style according to distance from target. My annotation is moving, with user.
I dont want to use remove / add annotation, because it causes "blinking".
Can it be done someway?
UPDATE:
I am adding code, how I am doing it right now
MKAnnotationView *av = [mapView viewForAnnotation:an];
if ([data->type isMemberOfClass:[UserAnnotationImage class]])
{
UIImage *img = [UIImage imageNamed: ((UserAnnotationImage *)data->type)->url];
[av setImage:img];
}
else if ([data->type isMemberOfClass:[UserAnnotationLabel class]])
{
UIView * v = [av viewWithTag:0];
v = ((UserAnnotationLabel *)data->type)->lbl;
av.frame = ((UserAnnotationLabel *)data->type)->lbl.frame;
}
else if ([data->type isMemberOfClass:[UserAnnotationView class]])
{
UIView * v = [av viewWithTag:0];
v = ((UserAnnotationView *)data->type)->view;
av.frame = ((UserAnnotationView *)data->type)->view.frame;
}
Sadly, its not working :(

Yes, basically you get a reference to the annotation view and update its contents directly.
Another way, if you have a custom annotation view class, is to have the annotation view monitor the changes it is interested in (or have something outside tell it) and update itself.
The first approach is simpler if you are using a plain MKAnnotationView or MKPinAnnotationView.
Wherever you detect that a change to the view is needed, get a reference to the view by calling the map view's viewForAnnotation instance method. This is not the same as calling the viewForAnnotation delegate method.
Once you have a reference to the view, you can modify as needed and the changes should appear immediately.
An important point is that the logic you use to update the view outside the delegate method and the logic you have in the viewForAnnotation delegate method must match. This is because the delegate method may get called later (after you've updated the view manually) by the map view and when it does, the code there should take the updated data into account.
The best way to do that is to have the annotation view construction code in a common method called both by the delegate method and where you update the view manually.
See change UIImage from MKAnnotation in the MKMapView for an example that updates just the annotation view's image.
For an example (mostly an idea for an approach) of updating the view using a custom annotation view class, see iPad Mapkit - Change title of "Current Location" which updates the view's pin color periodically (green, purple, red, green, purple, red, etc).
There are too many unknowns in your code to explain why it doesn't work. For example:
What is data? Is it annotation-specific (is it related to an)? What is type? Does it change after the annotation has been added to the map?
Why is data storing entire view objects like a UILabel or UIView instead of just the underlying data that you want to show in those views?
imageNamed requires the image to be a resource in the project (not any arbitrary url)
Don't use a tag of 0 (that's the default for all views). Start numbering from 1.
You get a view using viewWithTag but then replace it immediately with another view.
I'll instead give a more detailed but simple(r) example...
Assume you have an annotation class (the one that implements MKAnnotation) with these properties (in addition to coordinate, title, and subtitle):
#property (nonatomic, assign) BOOL haveImage;
#property (nonatomic, copy) NSString *labelText;
#property (nonatomic, copy) NSString *imageName;
#property (nonatomic, assign) CLLocationDistance distanceFromTarget;
To address the "important point" mentioned above (that the viewForAnnotation delegate method and the view-update-code should use the same logic), we'll create a method that is passed an annotation view and configures it as needed based on the annotation's properties. This method will then be called both by the viewForAnnotation delegate method and the code that manually updates the view when the annotation's properties change.
In this example, I made it so that the annotation view shows the image if haveImage is YES otherwise it shows the label. Additionally, the label's background color is based on distanceFromTarget:
-(void)configureAnnotationView:(MKAnnotationView *)av
{
MyAnnotationClass *myAnn = (MyAnnotationClass *)av.annotation;
UILabel *labelView = (UILabel *)[av viewWithTag:1];
if (myAnn.haveImage)
{
//show image and remove label...
av.image = [UIImage imageNamed:myAnn.imageName];
[labelView removeFromSuperview];
}
else
{
//remove image and show label...
av.image = nil;
if (labelView == nil)
{
//create and add label...
labelView = [[[UILabel alloc]
initWithFrame:CGRectMake(0, 0, 50, 30)] autorelease];
labelView.tag = 1;
labelView.textColor = [UIColor whiteColor];
[av addSubview:labelView];
}
if (myAnn.distanceFromTarget > 100)
labelView.backgroundColor = [UIColor redColor];
else
labelView.backgroundColor = [UIColor greenColor];
labelView.text = myAnn.labelText;
}
}
The viewForAnnotation delegate method would look like this:
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MyAnnotationClass class]])
{
static NSString *myAnnId = #"myann";
MKAnnotationView *av = [mapView dequeueReusableAnnotationViewWithIdentifier:myAnnId];
if (av == nil)
{
av = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:myAnnId] autorelease];
}
else
{
av.annotation = annotation;
}
[self configureAnnotationView:av];
return av;
}
return nil;
}
Finally, the place where the annotation's properties change and where you want to update the annotation view, the code would look something like this:
ann.coordinate = someNewCoordinate;
ann.distanceFromTarget = theDistanceFromTarget;
ann.labelText = someNewText;
ann.haveImage = YES or NO;
ann.imageName = someImageName;
MKAnnotationView *av = [mapView viewForAnnotation:ann];
[self configureAnnotationView:av];

Related

How to create a custom NSView for NSSavePanel in Cocoa MacOS objective C?

I need to add a save extension selector with a text label next to it to my NSSavePanel. In the screenshot attached I try to demonstrate that I succeeded in adding an NSComboBox to my panel with the function setAccessoryView. However I have no idea how to create a custom NSView, which includes both an NSComboBox and an NSTextView or equivalent. I found no tutorials on the internet (or if I found one it was extremely outdated) showing how to create custom NSViews in objective-C in Cocoa on MacOS.
How can I create a custom NSView containing a combobox and a text label? Or how can I add two "stock" NSViews to the same NSSavePanel? Please be as detailed in your answer as possible, as I have very limited objective-c experience.
You asked how to create an NSView in Objective-C with an NSTextField and an NSComboBox as subviews.
Basically, you could define them in Interface Builder and programmatically set the resulting view in Objective-C as the accessoryView of the NSSavePanel. Alternatively, the custom NSView could be created entirely in Objective-C, which is probably the easier option here.
After instantiating an NSView, you can use addSubview: to add an NSTextField and an NSComboBox accordingly. Then you can use NSLayoutConstraints to set up Auto Layout, which takes care of sizing the accessoryView and arranging the subviews properly based on the width of the dialog.
If you create the views programmatically and use Auto Layout, you must explicitly set translatesAutoresizingMaskIntoConstraints to NO.
Should you want to set the allowedContentTypes, a textual mapping of the displayed extension to UTType via a NSDictionary might be useful.
If you set the delegate of the NSComboBox to self, then you will be informed about changes of the user selection in the NSComboBox via comboBoxSelectionDidChange:.
If the things discussed are implemented appropriately in code, it might look something like this for a self-contained example:
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "ViewController.h"
#interface ViewController () <NSComboBoxDelegate>
#property (nonatomic, strong) NSSavePanel *savePanel;
#property (nonatomic, strong) NSDictionary<NSString *, UTType*> *typeMapping;
#end
#implementation ViewController
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
_typeMapping = #{
#"jpeg": UTTypeJPEG,
#"png": UTTypePNG,
#"tiff": UTTypeTIFF
};
}
return self;
}
- (NSView *)accessoryView {
NSTextField *label = [NSTextField labelWithString:#"Filetypes:"];
label.textColor = NSColor.lightGrayColor;
label.font = [NSFont systemFontOfSize:NSFont.smallSystemFontSize];
label.alignment = NSTextAlignmentRight;
NSComboBox *comboBox = [NSComboBox new];
comboBox.editable = NO;
for (NSString *extension in self.typeMapping.allKeys) {
[comboBox addItemWithObjectValue:extension];
}
[comboBox setDelegate:self];
NSView *view = [NSView new];
[view addSubview:label];
[view addSubview:comboBox];
comboBox.translatesAutoresizingMaskIntoConstraints = NO;
label.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:#[
[label.bottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:-12],
[label.widthAnchor constraintEqualToConstant:64.0],
[label.leadingAnchor constraintEqualToAnchor:view.leadingAnchor constant:0.0],
[comboBox.topAnchor constraintEqualToAnchor:view.topAnchor constant:8.0],
[comboBox.leadingAnchor constraintEqualToAnchor:label.trailingAnchor constant:8.0],
[comboBox.bottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:-8.0],
[comboBox.trailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:-20.0],
]];
return view;
}
- (void)comboBoxSelectionDidChange:(NSNotification *)notification {
NSComboBox *comboBox = notification.object;
NSString *selectedItem = comboBox.objectValueOfSelectedItem;
NSLog(#"### set allowedContentTypes to %# (%#)", selectedItem, self.typeMapping[selectedItem]);
[self.savePanel setAllowedContentTypes:#[ self.typeMapping[selectedItem] ]];
}
- (IBAction)onSave:(id)sender {
NSWindow *window = NSApplication.sharedApplication.windows.firstObject;
self.savePanel = [NSSavePanel new];
self.savePanel.accessoryView = [self accessoryView];
[self.savePanel beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
if (result != NSModalResponseOK) {
return;
}
NSURL *fileURL = self.savePanel.URL;
NSLog(#"### selectedFile: %#", fileURL);
}];
}
- (void)setRepresentedObject:(id)representedObject {
[super setRepresentedObject:representedObject];
}
#end
Finally, a screenshot of the above demo code in action looks like this:
Press Cmd-N to add a new file to your project. Choose a View file to add a xib file that has a custom view.
Open the xib file and add the controls to the custom view. Press the Add button in the project window toolbar to access the user interface elements.
Use the NSNib class to load the xib file and get the custom view.

Automatically click segmentControl with index 0 on ViewDidAppear

I am hoping to "automate" a click on the segmentController with the index of 0.
My tabBar-based app has multiple segmentControllers in a tab in the ViewDidAppear method, I would like to automatically have it "click" the first segmented controller.
if (segmentController.selectedSegmentIndex == 0) {
//stuff here
}
if (segmentController.selectedSegmentIndex == 1) {
//stuff here
}
Does anyone know how I might accomplish this? Thank you!
If you're creating it programmatically, you could lazy load it like this:
#interface ExampleViewController : UIViewController
#property (nonatomic, strong) UISegmentedControl *segmentedControl;
- (void)segmentedControlClicked:(UISegmentedControl *)segmentedControl;
#end
#implementation ExampleViewController
- (UISegmentedControl *)segmentedControl
{
if (!_segmentedControl)
{
NSArray *items = #[#"First", #"Second", #"Third"];
_segmentedControl = [[UISegmentedControl alloc] initWithItems:items];
[_segmentedControl addTarget:self
action:#selector(segmentedControlClicked:)
forControlEvents:UIControlEventValueChanged];
[_segmentedControl setSelectedSegmentIndex:0]; // Set Default selection
CGRect frame = _segmentedControl.frame;
frame.origin = CGPointMake(0.0f, 0.0f); // Move to wherever you need it
[self.view addSubview:_segmentedControl];
}
return _segmentedControl;
}
- (void)segmentedControlClicked:(UISegmentedControl *)segmentedControl
{
// Whatever your code is goes here...
}
#end
If you're wanting a method to be called also initially, you can call it within your viewDidLoad: method as such:
- (void)viewDidLoad
{
[self.segmentedControl setSelectedSegmentIndex:0]; // Set desired default index (optional if set in lazy load as shown above)
[self segmentedControlClicked:self.segmentedControl];
}
This would hence simulate a click on desired default index.
Be careful putting the above into viewDidAppear: (you could if you really wanted to) because anytime the view comes to the front, this method will be called (in example, if this view controller presents a modal view controller, once the modal is dismissed, this view controller's viewDidAppear: method will be called).
Cheers!
Set the selectedSegmentIndex property on your UISegmentedControl in your viewDidAppear (or viewDidLoad) method.
self.segmentedController.selectedSegemntIndex = 1;
UISegmentedControl Reference

Unique ID on NSViews

Is there any kind of ID that can be used and set in the .nib/.xib via Xcode that can be queried at runtime to identify a particular view instance from code?
In particular when having multiple copies of the same NSView subclass in our interface how can we tell which one we're currently looking at?
In Interface Builder, there is a way to set the "identifier" of an NSView. In this case, I'll use the identifier "54321" as the identifier string.
NSView Conforms to the NSUserInterfaceItemIdentification Protocol, which is a unique identifier as an NSString. You could walk through the view hierarchy and find the NSView with that identifier.
So, to build on this post about getting the list of NSViews, Get ALL views and subview of NSWindow, you could then find the NSView with the identifier you want:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSView *viewToFind = [self viewWithIdentifier:#"54321"];
}
- (NSView *)viewWithIdentifier:(NSString *)identifier
{
NSArray *subviews = [self allSubviewsInView:self.window.contentView];
for (NSView *view in subviews) {
if ([view.identifier isEqualToString:identifier]) {
return view;
}
}
return nil;
}
- (NSMutableArray *)allSubviewsInView:(NSView *)parentView {
NSMutableArray *allSubviews = [[NSMutableArray alloc] initWithObjects: nil];
NSMutableArray *currentSubviews = [[NSMutableArray alloc] initWithObjects: parentView, nil];
NSMutableArray *newSubviews = [[NSMutableArray alloc] initWithObjects: parentView, nil];
while (newSubviews.count) {
[newSubviews removeAllObjects];
for (NSView *view in currentSubviews) {
for (NSView *subview in view.subviews) [newSubviews addObject:subview];
}
[currentSubviews removeAllObjects];
[currentSubviews addObjectsFromArray:newSubviews];
[allSubviews addObjectsFromArray:newSubviews];
}
for (NSView *view in allSubviews) {
NSLog(#"View: %#, tag: %ld, identifier: %#", view, view.tag, view.identifier);
}
return allSubviews;
}
Or, since you are using an NSView subclass, you could set the "tag" of each view at runtime. (Or, you could set the identifier at run-time.) The nice thing about tag, is that there is a pre-built function for finding a view with a specific tag.
// set the tag
NSInteger tagValue = 12345;
[self.myButton setTag:tagValue];
// find it
NSButton *myButton = [self.window.contentView viewWithTag:12345];
Generic NSView objects cannot have their tag property set in Interface Builder. The tag method on NSView is a read-only method and can only be implemented in subclasses of NSView. NSView does not implement a setTag: method.
I suspect the other answers are referring to instances of NSControl which defines a -setTag: method and has an Interface Builder field to allow you to set the tag.
What you can do with generic views is use user-defined runtime attributes. This allows you to pre-set the values of properties in your view object. So if your view defined a property like so:
#property (strong) NSNumber* viewID;
Then in the user-defined attributes section of the Identity inspector in Interface Builder, you could add a property with the keypath viewID, the type Number and the value 123.
In your view's -awakeFromNib method, you can then access the value of the property. You will find that in the example above, the viewID property of your view will have been pre-set to 123.
Here's how to simulate "tags" in OSX without subclassing.
In iOS:
{
// iOS:
// 1. You add a tag to a view and add it as a subView, as in:
UIView *masterView = ... // the superview
UIView *aView = ... // a subview
aView.tag = 13;
[masterView addSubview:aView];
// 2. Later, to retrieve the tagged view:
UIView *aView = [masterView viewWithTag:13];
// returns nil if there's no subview with that tag
}
The equivalent in OSX:
#import <objc/runtime.h> // for associated objects
{
// OSX:
// 1. Somewhere early, create an invariant memory address
static void const *tag13 = &tag13; // put at the top of the file
// 2. Attach an object to the view to which you'll be adding the subviews:
NSView *masterView = ... // the superview
NSView *aView = ... // a subview
[masterView addSubview:aView];
objc_setAssociatedObject(masterView, tag13, aView, OBJC_ASSOCIATION_ASSIGN);
// 3. Later, to retrieve the "tagged" view:
NSView *aView = objc_getAssociatedObject(masterView, tag13);
// returns nil if there's no subview with that associated object "tag"
}
Edit: The associated object "key" (declared as const void *key) needs to be invariant. I'm using an idea by Will Pragnell (https://stackoverflow.com/a/18548365/236415).
Search Stack Overflow for other schemes for making the key.
Here's a simple way get NSView tags in OSX without subclassing.
Although NSView's tag property is read-only, some objects
that inherit from NSView have a read/write tag property.
For example, NSControl and NSImageView have a r/w tag properties.
So, simply use NSControl instead of NSView, and disable (or ignore)
NSControl things.
- (void)tagDemo
{
NSView *myView1 = [NSView new];
myView1.tag = 1; // Error: "Assignment to readonly property"
// ---------
NSControl *myView2 = [NSControl new]; // inherits from NSView
myView2.tag = 2; // no error
myView2.enabled = NO; // consider
myView2.action = nil; // consider
// ---------
NSImageView *myView3 = [NSImageView new]; // inherits from NSControl
myView3.tag = 3; // no error
myView3.enabled = NO; // consider
myView3.action = nil; // consider
}
Later, if you use viewWithTag: to fetch the view, be sure to specify NSControl (or NSImageView) as the returned type.

Working on custom component: subclass UIView or UIViewController?

I'm working on a custom implementation of UISegmentedControl.
I'd like to create a component that able to receive config data and from which obtain a custom View similar to UISegmentedControl.
I started subclassing a UIView and i can create a custom UISegmentedControl with this code:
CustomSegment *segment = [[CustomSegment alloc]
initWithTitles:[NSArray arrayWithObjects:#"one",#"two",nil]];
[self.window addSubview:segment];
But now i'd like to improve my class and add some more customizable parameters to it.
For example i'd like add a custom separators, define the button fonts and so on... here my doubt:
Is it better to work on a UIView subClass or you suggest me to subclass a UIViewController, where i can manage View hierarchy in method like -(void)loadView and -(void)viewDidLoad ?
In a simple UIView subclass, when i launch the custom init method, i setup immediately subviews... while using a UIViewController i can call custom init and define how my subview is builded into -(void)loadView.
Don't use an UIViewController, just extend the UIView class like you did and keep extending its functionality.
Remember to save a pointer to each subview you add (i.e. buttons) in order to be able to access them later.
Define custom setters, for example, a custom setter for changing a button label title would be:
- (void) setButton1Title:(NSString*)str forState:(UIControlState)state{
//You can add some control here
if ([str length] > 20) return;
[_button1 setTitle:str forState:state]; //_button1 is my reference to the button
}
And so on. Don't provide direct access to your subviews, use methods instead.
Also, you can use "layoutSubviews" method to define how your views are going to be displayed in your custom view.
Hope it helps you.
Edit: In your case, I don't see why using lauoutSubviews method but I want to show you what I was trying to say.
Lets say that for example I need to create an UIView class to represent a "Contact" object in my application.
This is what I would do:
#interface ContactView : UIView{
UILabel* _nameLabel;
UILabel* _ageLabel;
Contact* _contact;
}
#property (retain) Contact* contact;
#end
#implementation ContactView
#synthetize contact = _contact;
-(id)initWithContact:(Contact*)c{
self = [super init];
if (self) {
_nameLabel = [[UILabel alloc] init];
_nameLabel.frame = CGRectZero;
[self addSubview:_nameLabel];
[_nameLabel release];
_ageLabel = [[UILabel alloc] init];
_ageLabel.frame = CGRectZero;
[self addSubview:_ageLabel];
[_ageLabel release];
self.contact = c;
}
}
- (void) layoutSubviews{
[super layoutSubviews];
_nameLabel.frame = CGRectMake(0.0f, 0.0f, 200.0f, 25.0f);
_ageLabel.frame = CGRectMake(0.0f, 25.0f, 200.0f, 25.0f);
if (self.contact){
_nameLabel.text = self.contact.name;
_ageLabel.text = self.contact.age;
}else{
_nameLabel.text = #"Unavailable";
_ageLabel.text = #"Unavailable";
}
}
- (void) setContact:(Contact*)c{
self.contact = c;
[self layoutSubviews];
}
#end
Check out how the "layoutSubiews" is used to set the correct frame and data to the labels.
Usually, I use it a lot when creating custom UITableViewCells where you have to reuse the view.
Let me know if I'm being confusing.

Subclasses of MKAnnotationView

I was wondering if anyone knows of any subclasses for the MKAnnotationView class. On apples documentation they say one example is the MKPinAnnotationView so I was wondering if there were other pre-created subclasses like the one used to track the devices current location. If anyone has tips for creating my own subclass of the MKAnnotationView class that be great as well.
Thanks,
bubster
In case anyone is still interested in this:
You can get all the subclasses of a class using the Objective-C runtime functions, as described here: http://cocoawithlove.com/2010/01/getting-subclasses-of-objective-c-class.html
Other classes that inherit from MKAnnotationView are:
MKTransitCalloutView, MKAdAnnotationView, MKUserLocationView, MKUserLocationBreadCrumbView, and MKPinAnnotationView
where MKPinAnnotationView is the only one that is documented. All others are private classes that Apple uses internally.
I don't know of any other templates, but that does not mean that they don't exist. :)
Anyway, here's how to create custom ones:
Create a new class conforming to the MKAnnotation protocol. You will need to have two instance variables of type NSString* named title and subtitle and one of type CLLocationCoordinate2D named coordinate and an appropriate setter method (e.g. property). Those strings are going to be displayed in the callout. In the delegate of your mapView, implement the method -mapView:viewForAnnotation: in a similar way as you would implement the datasource for a UITableView. That is, dequeueing an annotationView by an identifier, setting the new properties (e.g. a button of type UIButtonTypeDetailDisclosure for the right accessory view). You will want to add an image to display underneath the offset. Just use the image property of your MKAnnotationView. The center of your custom image will be placed at the coordinate specified, so you may want to give an offset: aView.centerOffset = CGPointMake(0, -20)
Here is some sample code:
- (MKAnnotationView *) mapView: (MKMapView *) mapView viewForAnnotation: (id<MKAnnotation>) annotation {
// reuse a view, if one exists
MKAnnotationView *aView = [mapView dequeueReusableAnnotationViewWithIdentifier:#"pinView"];
// create a new view else
if (!aView) {
aView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"pinView"];
}
// now configure the view
aView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[(UIButton*)aView.rightCalloutAccessoryView addTarget:self action:#selector(showDetails:) forControlEvents:UIControlEventTouchUpInside];
aView.canShowCallout = YES;
aView.enabled = YES;
aView.image = [UIImage imageNamed:#"green_pin.png"];
aView.centerOffset = CGPointMake(0, -20);
return aView;
}