How to print a control hierarchy in Cocoa? - cocoa

Carbon had a useful function called DebugPrintControlHierarchy.
Is there something similar for NSView or NSWindow?

I don't know what exactly DebugPrintControlHierarchy printed, but NSView has a useful method call _subtreeDescription which returns a string describing the entire hierarchy beneath the receiver, include classes, frames, and other useful information.
Don't be scared about the leading _ underscore. It's not public API, but it is sanctioned for public use in gdb. You can see it mentioned in the AppKit release notes along with some sample output.

Here's the guts of an NSView category I built awhile back:
+ (NSString *)hierarchicalDescriptionOfView:(NSView *)view
level:(NSUInteger)level
{
// Ready the description string for this level
NSMutableString * builtHierarchicalString = [NSMutableString string];
// Build the tab string for the current level's indentation
NSMutableString * tabString = [NSMutableString string];
for (NSUInteger i = 0; i <= level; i++)
[tabString appendString:#"\t"];
// Get the view's title string if it has one
NSString * titleString = ([view respondsToSelector:#selector(title)]) ? [NSString stringWithFormat:#"%#", [NSString stringWithFormat:#"\"%#\" ", [(NSButton *)view title]]] : #"";
// Append our own description at this level
[builtHierarchicalString appendFormat:#"\n%#<%#: %p> %#(%li subviews)", tabString, [view className], view, titleString, [[view subviews] count]];
// Recurse for each subview ...
for (NSView * subview in [view subviews])
[builtHierarchicalString appendString:[NSView hierarchicalDescriptionOfView:subview
level:(level + 1)]];
return builtHierarchicalString;
}
- (void)logHierarchy
{
NSLog(#"%#", [NSView hierarchicalDescriptionOfView:self
level:0]);
}
Usage
Dump this into an NSView category, dump this in it. Include the category header wherever you want to use it, then call [myView logHierarchy]; and watch it go.

Swift 4.
macOS:
extension NSView {
// Prints results of internal Apple API method `_subtreeDescription` to console.
public func dump() {
Swift.print(perform(Selector(("_subtreeDescription"))))
}
}
iOS:
extension UIView {
// Prints results of internal Apple API method `recursiveDescription` to console.
public func dump() {
Swift.print(perform(Selector(("recursiveDescription"))))
}
}
Usage (in debugger): po myView.dump()

Related

NSTextFinder + programmatically changing the text in NSTextView

I have a NSTextView for which I want to use the find bar. The text is selectable, but not editable. I change the text in the text view programatically.
This setup can crash when NSTextFinder tries to select the next match after the text was changed. It seems NSTextFinder hold on to outdated ranges for incremental matches.
I tried several methods of changing the text:
[textView setString:#""];
or
NSTextStorage *newStorage = [[NSTextStorage alloc] initWithString:#""];
[textView.layoutManager replaceTextStorage:newStorage];
or
[textView.textStorage beginEditing];
[textView.textStorage setAttributedString:[[NSAttributedString alloc] initWithString:#""]];
[textView.textStorage endEditing];
Only replaceTextStorage: calls -[NSTextFinder noteClientStringWillChange]. None of the above invokes -[NSTextFinder cancelFindIndicator].
Even with NSTextFinder notified about the text change it can crash on Find Next (command-G).
I have also tried creating my own NSTextFinder instance as suggested in this post. Even though NSTextView does not implement the NSTextFinderClient protocol this works and fails just the same as without the NSTextFinder instance.
What is the correct way to use NSTextFinder with NSTextView?
I had the same problem with the text view in my app, and what makes it even more annoying is that all "solutions" you find on the internet are either incorrect or at least incomplete. So here is my contribution.
When you set textView.useFindBar = YES in a NSTextView, this text view creates a NSTextFinder internally, and forwards the search/replace commands to it. Unfortunately, NSTextView does not seem to handle correctly the changes you make programmatically to its associated NSTextStorage, which causes the crashes you mention.
If you want to change this behavior, creating your private NSTextFinder is not enough: you also need to avoid the use by the text view of its default text finder, otherwise conflicts will occur and the new text finder won't be of much use.
To do this, you have to subclass NSTextView:
#interface MyTextView : NSTextView
- (void) resetTextFinder; // A method to reset the view's text finder when you change the text storage
#end
And in your text view, you have to override the responder methods used for controlling the text finder:
#interface MyTextView () <NSTextFinderClient>
{
NSTextFinder* _textFinder; // define your own text finder
}
#property (readonly) NSTextFinder* textFinder;
#end
#implementation MyTextView
// Text finder command validation (could also be done in method validateUserInterfaceItem: if you prefer)
- (BOOL) validateMenuItem:(NSMenuItem *)menuItem
{
BOOL isValidItem = NO;
if (menuItem.action == #selector(performTextFinderAction:)) {
isValidItem = [self.textFinder validateAction:menuItem.tag];
}
// validate other menu items if needed
// ...
// and don't forget to call the superclass
else {
isValidItem = [super validateMenuItem:menuItem];
}
return isValidItem;
}
// Text Finder
- (NSTextFinder*) textFinder
{
// Create the text finder on demand
if (_textFinder == nil) {
_textFinder = [[NSTextFinder alloc] init];
_textFinder.client = self;
_textFinder.findBarContainer = [self enclosingScrollView];
_textFinder.incrementalSearchingEnabled = YES;
_textFinder.incrementalSearchingShouldDimContentView = YES;
}
return _textFinder;
}
- (void) resetTextFinder
{
if (_textFinder != nil) {
// Hide the text finder
[_textFinder cancelFindIndicator];
[_textFinder performAction:NSTextFinderActionHideFindInterface];
// Clear its client and container properties
_textFinder.client = nil;
_textFinder.findBarContainer = nil;
// And delete it
_textFinder = nil;
}
}
// This is where the commands are actually sent to the text finder
- (void) performTextFinderAction:(id<NSValidatedUserInterfaceItem>)sender
{
[self.textFinder performAction:sender.tag];
}
#end
In your text view, you still need to set properties usesFindBar and incrementalSearchingEnabled to YES.
And before changing the view's text storage (or text storage contents) you just need to call [myTextView resetTextFinder]; to recreate a brand new text finder for your new content the next time you will do a search.
If you want more information about NSTextFinder, the best doc I have seen is in the AppKit Release Notes for OS X 10.7
The solution I had come up with seems rather similar to the one offered by #jlj. In both solutions NSTextView is used as client of NSTextFinder.
It seems that the main difference is that I don't hide the find bar on text change. I also hold onto my NSTextFinder instance. To do so I need to call [textFinder noteClientStringWillChange].
Changing text:
NSTextView *textView = self.textView;
NSTextFinder *textFinder = self.textFinder;
[textFinder cancelFindIndicator];
[textFinder noteClientStringWillChange];
[textView setString:#"New text"];
The rest of the view controller code looks like this:
- (void)viewDidLoad
{
[super viewDidLoad];
NSTextFinder *textFinder = [[NSTextFinder alloc] init];
[textFinder setClient:(id < NSTextFinderClient >)textView];
[textFinder setFindBarContainer:[textView enclosingScrollView]];
[textView setUsesFindBar:YES];
[textView setIncrementalSearchingEnabled:YES];
self.textFinder = textFinder;
}
- (void)viewWillDisappear
{
NSTextFinder *textFinder = self.textFinder;
[textFinder cancelFindIndicator];
[super viewWillDisappear];
}
- (id)supplementalTargetForAction:(SEL)action sender:(id)sender
{
id target = [super supplementalTargetForAction:action sender:sender];
if (target != nil) {
return target;
}
if (action == #selector(performTextFinderAction:)) {
target = self.textView;
if (![target respondsToSelector:action]) {
target = [target supplementalTargetForAction:action sender:sender];
}
if ((target != self) && [target respondsToSelector:action]) {
return target;
}
}
return nil;
}

How do I override drag reception in an NSTextField?

I'm writing this in Swift. I have an NSTextField I've assigned a class in IB defined by:
class MyTextField : NSTextField, NSDraggingDestination {
I've overridden draggingEntered, draggingUpdated, prepareForDragOperation, performDragOperation in the subclass, but none of these is ever called and the system just puts stuff in the field as it sees fit. I want to handle the drag because, among other things, I don't want the default behavior of pasting a URL into the field if the user drags a file to it. Instead, if he does that, I want to get the display name of the file and use that instead.
What am I missing?
One of the responsibilities of any object implementing the <draggingDestination> protocol is to maintain an array of data-types which informs others what sort of data will trigger the methods you mention in your question. To allow your subclass to deal with drags from Finder or the desktop, I've found you need to register for three pasteboard types.
/* Sorry, not using Swift yet */
// MyNSTextField.m
- (void)awakeFromNib {
[self registerForDraggedTypes:#[NSPasteboardTypeString,
NSURLPboardType,
NSFilenamesPboardType]];
}
At least on OS X 10.9, this is sufficient to fire your draggingEntered method. If all you want on the pasteboard is the filename, rather than the full URL or path, you need to (i) extract the name, (ii) clear the pasteboard and (iii) add just the name back onto the pasteboard:
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
NSDragOperation operation = NSDragOperationNone;
NSPasteboard *pBoard = [sender draggingPasteboard];
NSArray *array = [pBoard readObjectsForClasses:#[[NSURL class], [NSString class]]
options:nil];
if ([array count] > 0) {
NSString *filename;
if ([[array firstObject] isKindOfClass:[NSURL class]]) {
// Possibly a file dragged from Finder
NSURL *url = [array firstObject];
filename = [[url pathComponents] lastObject];
} else if ([[array firstObject] isKindOfClass:[NSString class]]) {
// Possibly a file dragged from the desktop
NSString *path = [array firstObject];
BOOL isPath = [[NSFileManager defaultManager] fileExistsAtPath:path];
if (isPath) {
filename = [path lastPathComponent];
}
}
if (filename) {
[pBoard clearContents];
[pBoard setData:[filename dataUsingEncoding:NSUTF8StringEncoding]
forType:NSPasteboardTypeString];
operation = NSDragOperationGeneric;
}
}
return operation;
}
On occasion the drag into the text field will happen so quickly that the above method is not triggered, in which case you're back to the same problem. One way around this is to implement the following text field delegate method:
// From NSTextFieldDelegate Protocol
- (void)textDidChange:(NSNotification *)notification
In this method you can compare the contents of your text field, with the contents of the pasteboard, if you're field now contains a valid system path and this path matches the contents of the pasteboard, you know you need to adjust the string in the text field. Fortunately, this seems to happen so quickly that it looks just like a normal paste operation.

What does OSX do when I customize an NSTableView cell?

I am trying to customize an NSImageCell for NSTableView using NSArrayController and bindings to change the background of the cell which is selected. So, I created two NSImage images and retain them as normalImage and activeImage in the cell instance, which means I should release these two images when the cell calls its dealloc method. And I override
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
and
- (void) setObjectValue:(id) inObject
But I find that when I click any cell in the tableview, the cell's dealloc method is called.
So I put NSLog(#"%#", self); in the dealloc method and - (void)drawInteriorWithFrame:inView: and I find that these two instance are not same.
Can anyone tell me why dealloc is called every time I click any cell? Why are these two instances not the same? What does OS X do when I customize the cell in NSTableView?
BTW: I found that the -init is called only once. Why?
EDIT:
My cell code
#implementation SETableCell {
NSImage *_bgNormal;
NSImage *_bgActive;
NSString *_currentString;
}
- (id)init {
if (self = [super init]) {
NSLog(#"setup: %#", self);
_bgNormal = [[NSImage imageNamed:#"bg_normal"] retain];
_bgActive = [[NSImage imageNamed:#"bg_active"] retain];
}
return self;
}
- (void)dealloc {
// [_bgActive release]; _bgActive = nil;
// [_bgNormal release]; _bgNormal = nil;
// [_currentString release]; _currentString = nil;
NSLog(#"dealloc: %#", self);
[super dealloc];
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSLog(#"draw: %#", self);
NSPoint point = cellFrame.origin;
NSImage *bgImg = self.isHighlighted ? _bgActive : _bgNormal;
[bgImg drawAtPoint:p fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
NSPoint strPoint = cellFrame.origin;
strPoint.x += 30;
strPoint.y += 30;
[_currentString drawAtPoint:strPoint withAttributes:nil];
}
- (void) setObjectValue:(id) inObject {
if (inObject != nil && ![inObject isEqualTo:_currentString]) {
[self setCurrentInfo:inObject];
}
}
- (void)setCurrentInfo:(NSString *)info {
if (_currentString != info) {
[_currentString release];
_currentString = [info copy];
}
}
#end
As a normal recommendation, you should move to ARC as it takes cares of most of the memory management tasks that you do manually, like retain, releases. My answers will assume that you are using manual memory management:
Can anyone tell me why dealloc is called every time I click any cell ?
The only way for this to happen, is if you are releasing or auto-releasing your cell. If you are re-using cells, they shouldn't be deallocated.
Why these tow instance are not same ?
If you are re-using them, the cell that you clicked, and the cell that has been deallocated, they should be different. Pay close attention to both your questions, in one you assume that you are releasing the same cell when you click on it, on the second you are seeing that they are different.
What does Apple do when I custom the cell in NSTableView ?
Apple as a company? Or Apple as in the native frameworks you are using? I am assuming you are going for the second one: a custom cell is just a subclass of something that the NSTableView is expecting, it should behave the same as a normal one plus your custom implementation.
BTW: I found that the init is called only once, and why ?
Based on this, you are probably re-using cells, and only in the beginning they are actually being initialised.
It would be very useful to see some parts of your code:
Your Cell's code
Your NSTableView cell's creation code.

UIBarButtonItem frame? [duplicate]

UIBarButtonItem does not extend UIView, so there is nothing like a frame property.
But is there any way I can get what is it's CGRect frame, relative to the application UIWindow?
Do you like to use private APIs? If yes,
UIView* view = thatItem.view;
return [view convertRect:view.bounds toView:nil];
Of course no one wants this when targeting the AppStore. A more unreliable method, and also uses undocumented features, but will pass Apple's test, is to loop through the subviews to look for the corresponding button item.
NSMutableArray* buttons = [[NSMutableArray alloc] init];
for (UIControl* btn in theToolbarOrNavbar.subviews)
if ([btn isKindOfClass:[UIControl class]])
[buttons addObject:btn];
UIView* view = [buttons objectAtIndex:index];
[buttons release];
return [view convertRect:view.bounds toView:nil];
The index is the index to your bar item in the array of .items, after removing all blank items. This assumes the buttons are arranged in increasing order, which may not be. A more reliable method is to sort the buttons array in increasing .origin.x value. Of course this still assumes the bar button item must inherit the UIControl class, and are direct subviews of the toolbar/nav-bar, which again may not be.
As you can see, there are a lot of uncertainty when dealing with undocumented features. However, you just want to pop up something under the finger right? The UIBarButtonItem's .action can be a selector of the form:
-(void)buttonClicked:(UIBarButtonItem*)sender event:(UIEvent*)event;
note the event argument — you can obtain the position of touch with
[[event.allTouches anyObject] locationInView:theWindow]
or the button view with
[[event.allTouches anyObject] view]
Therefore, there's no need to iterate the subviews or use undocumented features for what you want to do.
I didn't see this option posted (which in my opinion is much simpler), so here it is:
UIView *barButtonView = [barButtonItem valueForKey:#"view"];
In iOS 3.2, there's a much easier way to show an Action Sheet popover from a toolbar button. Merely do something like this:
- (IBAction)buttonClicked:(UIBarButtonItem *)sender event:(UIEvent *)event
{
UIActionSheet *popupSheet;
// Prepare your action sheet
[popupSheet showFromBarButtonItem:sender animated:YES];
}
This is the implementation I use for my WEPopover project: (https://github.com/werner77/WEPopover):
#implementation UIBarButtonItem(WEPopover)
- (CGRect)frameInView:(UIView *)v {
UIView *theView = self.customView;
if (!theView.superview && [self respondsToSelector:#selector(view)]) {
theView = [self performSelector:#selector(view)];
}
UIView *parentView = theView.superview;
NSArray *subviews = parentView.subviews;
NSUInteger indexOfView = [subviews indexOfObject:theView];
NSUInteger subviewCount = subviews.count;
if (subviewCount > 0 && indexOfView != NSNotFound) {
UIView *button = [parentView.subviews objectAtIndex:indexOfView];
return [button convertRect:button.bounds toView:v];
} else {
return CGRectZero;
}
}
#end
As long as UIBarButtonItem (and UITabBarItem) does not inherit from UIView—for historical reasons UIBarItem inherits from NSObject—this craziness continues (as of this writing, iOS 8.2 and counting ... )
The best answer in this thread is obviously #KennyTM's. Don't be silly and use the private API to find the view.
Here's a oneline Swift solution to get an origin.x sorted array (like Kenny's answer suggests):
let buttonFrames = myToolbar.subviews.filter({
$0 is UIControl
}).sorted({
$0.frame.origin.x < $1.frame.origin.x
}).map({
$0.convertRect($0.bounds, toView:nil)
})
The array is now origin.x sorted with the UIBarButtonItem frames.
(If you feel the need to read more about other people's struggles with UIBarButtonItem, I recommend Ash Furrow's blog post from 2012: Exploring UIBarButtonItem)
I was able to get Werner Altewischer's WEpopover to work by passing up the toolbar along with the
UIBarButton:
Mod is in WEPopoverController.m
- (void)presentPopoverFromBarButtonItem:(UIBarButtonItem *)item toolBar:(UIToolbar *)toolBar
permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections
animated:(BOOL)animated
{
self.currentUIControl = nil;
self.currentView = nil;
self.currentBarButtonItem = item;
self.currentArrowDirections = arrowDirections;
self.currentToolBar = toolBar;
UIView *v = [self keyView];
UIButton *button = nil;
for (UIView *subview in toolBar.subviews)
{
if ([[subview class].description isEqualToString:#"UIToolbarButton"])
{
for (id target in [(UIButton *)subview allTargets])
{
if (target == item)
{
button = (UIButton *)subview;
break;
}
}
if (button != nil) break;
}
}
CGRect rect = [button.superview convertRect:button.frame toView:v];
[self presentPopoverFromRect:rect inView:v permittedArrowDirections:arrowDirections animated:animated];
}
-(CGRect) getBarItemRc :(UIBarButtonItem *)item{
UIView *view = [item valueForKey:#"view"];
return [view frame];
}
You can get it from the UINavigationBar view. The navigationBar is a UIView which has 2 or 3 custom subviews for the parts on the bar.
If you know that the UIBarButtonItem is currently shown in the navbar on the right, you can get its frame from navbar's subviews array.
First you need the navigationBar which you can get from the navigationController which you can get from the UIViewController. Then find the right most subview:
UINavigationBar* navbar = curViewController.navigationController.navigationBar;
UIView* rightView = nil;
for (UIView* v in navbar.subviews) {
if (rightView==nil) {
rightView = v;
} else if (v.frame.origin.x > rightView.frame.origin.x) {
rightView = v; // this view is further right
}
}
// at this point rightView contains the right most subview of the navbar
I haven't compiled this code so YMMV.
This is not the best solution and from some point of view it's not right solution and we can't do like follow because we access to object inside UIBarBattonItem implicitly, but you can try to do something like:
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
[button setImage:[UIImage imageNamed:#"Menu_Icon"] forState:UIControlStateNormal];
[button addTarget:self action:#selector(didPressitem) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:button];
self.navigationItem.rightBarButtonItem = item;
CGPoint point = [self.view convertPoint:button.center fromView:(UIView *)self.navigationItem.rightBarButtonItem];
//this is like view because we use UIButton like "base" obj for
//UIBarButtonItem, but u should note that UIBarButtonItem base class
//is NSObject class not UIView class, for hiding warning we implicity
//cast UIBarButtonItem created with UIButton to UIView
NSLog(#"point %#", NSStringFromCGPoint(point));
as result i got next:
point {289, 22}
Before implement this code, be sure to call [window makeKeyAndVisible] in your Applition delegate application:didFinishLaunchingWithOptions: method!
- (void) someMethod
{
CGRect rect = [barButtonItem convertRect:barButtonItem.customview.bounds toView:[self keyView]];
}
- (UIView *)keyView {
UIWindow *w = [[UIApplication sharedApplication] keyWindow];
if (w.subviews.count > 0) {
return [w.subviews objectAtIndex:0];
} else {
return w;
}
}
I handled it as follows:
- (IBAction)buttonClicked:(UIBarButtonItem *)sender event:(UIEvent *)event
{
UIView* view = [sender valueForKey:#"view"]; //use KVO to return the view
CGRect rect = [view convertRect:view.bounds toView:self.view];
//do stuff with the rect
}

ipad: predictive search in a popover

I want to implement this
1) when user start typing in a textfield a popOver flashes and shows the list of items in a table view in the popover as per the string entered in textfield.
2) Moreover this data should be refreshed with every new letter entered.
kind of predictive search.
Please help me with this and suggest possible ways to implement this.
UISearchDisplayController does most of the heavy lifting for you.
Place a UISearchBar (not a UITextField) in your view, and wire up a UISearchDisplayController to it.
// ProductViewController.h
#property IBOutlet UISearchBar *searchBar;
#property ProductSearchController *searchController;
// ProductViewController.m
- (void) viewDidLoad
{
[super viewDidLoad];
searchBar.placeholder = #"Search products";
searchBar.showsCancelButton = YES;
self.searchController = [[[ProductSearchController alloc]
initWithSearchBar:searchBar
contentsController:self] autorelease];
}
I usually subclass UISearchDisplayController and have it be it's own delegate, searchResultsDataSource and searchResultsDelegate. The latter two manage the result table in the normal manner.
// ProductSearchController.h
#interface ProductSearchController : UISearchDisplayController
<UISearchDisplayDelegate, UITableViewDelegate, UITableViewDataSource>
// ProductSearchController.m
- (id)initWithSearchBar:(UISearchBar *)searchBar
contentsController:(UIViewController *)viewController
{
self = [super initWithSearchBar:searchBar contentsController:viewController];
self.contents = [[NSMutableArray new] autorelease];
self.delegate = self;
self.searchResultsDataSource = self;
self.searchResultsDelegate = self;
return self;
}
Each keypress in the searchbar calls searchDisplayController:shouldReloadTableForSearchString:. A quick search can be implemented directly here.
- (BOOL) searchDisplayController:(UISearchDisplayController*)controller
shouldReloadTableForSearchString:(NSString*)searchString
{
// perform search and update self.contents (on main thread)
return YES;
}
If your search might take some time, do it in the background with NSOperationQueue. In my example, ProductSearchOperation will call showSearchResult: when and if it completes.
// ProductSearchController.h
#property INSOperationQueue *searchQueue;
// ProductSearchController.m
- (BOOL) searchDisplayController:(UISearchDisplayController*)controller
shouldReloadTableForSearchString:(NSString*)searchString
{
if (!searchQueue) {
self.searchQueue = [[NSOperationQueue new] autorelease];
searchQueue.maxConcurrentOperationCount = 1;
}
[searchQueue cancelAllOperations];
NSInvocationOperation *op = [[[ProductSearchOperation alloc]
initWithController:self
searchTerm:searchString] autorelease];
[searchQueue addOperation:op];
return NO;
}
- (void) showSearchResult:(NSMutableArray*)result
{
self.contents = result;
[self.searchResultsTableView
performSelectorOnMainThread:#selector(reloadData)
withObject:nil waitUntilDone:NO];
}
It sounds like you have a pretty good idea of an implementation already. My suggestion would be to present a UITableView in a popover with the search bar at the top, then simply drive the table view's data source using the search term and call reloadData on the table view every time the user types into the box.

Resources