13

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?

Eimantas
  • 48,927
  • 17
  • 132
  • 168
Jay
  • 6,572
  • 3
  • 37
  • 65

4 Answers4

15

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];
Community
  • 1
  • 1
Chris Livdahl
  • 4,662
  • 2
  • 38
  • 33
12

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.

Rob Keniger
  • 45,830
  • 6
  • 101
  • 134
  • That sounds great, thanks for the hint! I'll try that. Alas, nothing pre-defined in the vanilla Cocoa NSView that can be used but I guess I'm still thinking too much in a PowerPlant/MFC way ;-) – Jay Dec 14 '11 at 15:16
  • 1
    Answer below does work for a generic view. Simply set Identifer in xib and access it programmatically with view.identifier – Colin Aug 27 '13 at 18:09
  • Any idea what's the logic behind tag being readonly in NSView? – Mercurial Apr 22 '15 at 18:58
  • Somehow I couldn't get my NSView subclass show the new member field in Interface Builder. Eventually decided to use the view.identifier, as I needed to find certain controls only once in windowDidLoad, so analyzing strings there was fine. – mojuba Aug 02 '15 at 00:21
  • FWIW, in certain situations and knowing my view hierarchy I now sometimes resort to checking all subviews (or parents) of an *entry-view* for a view of a certain class. Often helps to e.g. find '*that-one-textfield-in-my-collection-view-item*' or '*that-parent-clipview-of-my-content-view*' – Jay Dec 06 '16 at 10:07
2

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.

Jeff
  • 2,659
  • 1
  • 22
  • 41
-1

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.

Jeff
  • 2,659
  • 1
  • 22
  • 41
  • Can't set the tag on `NSView` so that won't work in the general case.. – Jay Nov 16 '20 at 09:58
  • Jay, I'm not suggesting you use `NSView`. Instead, use a subclass that allows setting the tag. You will still have all the `NSView` parameters. I've used it in many cases and it works great. My suggestion solves the OPs issue, so if you did the -1, please remove it. – Jeff Nov 19 '20 at 09:57