Notification when content changes in NSOutlineView subclass - objective-c

I'd like to subclass NSOutlineView to show a label in the middle of itself when there is no content yet. Much like the inspectors in XCode:
Obviously I can't use the delegate methods, because I'm implementing this as a subclass and I have to be able to set the delegate to something else when using this class.
I didn't find any notifications that I could observe, except changes in the bounds property, but that's not very reliable.

I ended up overriding several methods of NSOutlineView to inject my code. It's not a very elegant solution, but it works. If anyone has a better solution, let me know and I might accept your answer instead.
- (void)updateEmptyLabelVisibility {
int r = [[self dataSource] outlineView:self numberOfChildrenOfItem:nil];
BOOL hasRows = r > 0;
_emptyLabel.hidden = hasRows;
}
- (void)reloadItem:(id)item {
[super reloadItem:item];
[self updateEmptyLabelVisibility];
}
- (void)reloadData {
[super reloadData];
[self updateEmptyLabelVisibility];
}
- (void)awakeFromNib {
[super awakeFromNib];
[self updateEmptyLabelVisibility];
}
- (void)endUpdates {
[super endUpdates];
[self updateEmptyLabelVisibility];
}

Related

Objective-C how to call a method after self is initialized inside the object file?

Is there any way to know when a custom object is finished with being initialized from inside the object's file? Or let me rephrase the question, why can't I call any method inside this method?
- (id)initWithCoder:(NSCoder *)coder {
//NSLog(#"initWithCoder inside CustomObject (subclass of UIView)");
self = [super initWithCoder:coder];
if (self) {
//... initialization here
[self visibleEmptyButton]; //why does this method never get called?
}
return self;
}
EDIT:
-(void)viewDidLoad{
NSLog(#"viewDidLoad inside CustomObject(subclass of UIView) is called"); //It never gets called
[self viewDidLoad];
//initialization here...
}
(If the class you are init-ing is a subclass of UIViewController) Changing and setting things in the screen should be done after the view is loaded. Try doing it in this method:
- (void)viewDidLoad {
[super viewDidLoad];
[self visibleEmptyButton];
//Do the additional view altering here
}
If this method doesn't exist yet you can just add it to the .m file (no need to add it to the .h file).
In lieu of you're edit you could simply move the call to the UIViewController:
- (void)viewDidLoad {
[super viewDidLoad];
[TheInstanceOfYourViewClass visibleEmptyButton];
}
Also, to avoid making a whole bunch of small subview related methods public it often makes sense to create one method to handle the initial visual states.

How to determine that user finished vertical scrolling of tableview in objective c

I want to do some things when user finished vertical scrolling of table view. Does any body know how to determine that period?
You need your view controller to become a delegate of UIScrollView: UIScrollViewDelegate
In your delegate you can implement the following methods to help determine the end state:
- (void) scrollViewDidScroll:(UIScrollView *)scrollView{
}
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
}
There are also two properties of UIScrollView that can help:
scrollView.isDragging
scrollView.isDecelerating
Just note that there are several end 'possibilities' for the scroll view. If there is no deceleration, scrollViewDidEndDecelerating won't be called, only scrollViewDidEndDragging. However, if there is deceleration, both will be called. You can use the decelerate var in scrollViewDidEndDragging to help determine when to execute your code. For this reason, it's usually a good idea to have a separate method that's called by these delegate methods.
#implementation YourViewController
{
BOOL shouldDoYourTask;
}
-(void)performYourTask{
//Do Your Stuff
shouldDoYourTask = YES;
}
#pragma mark - ScrollView Delegate
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate
{
if (!decelerate && shouldDoYourTask)
{
[self performYourTask];
shouldDoYourTask = NO;
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (shouldDoYourTask) {
[self performYourTask];
shouldDoYourTask = NO;
}
}

Creating No Empty Selections in NSCollectionView

I have set up an NSCollectionView in a cocoa application. I have subclassed the collection view's NSCollectionViewItem to send me a custom NSNotification when one of its views it selected / deselected. I register to receive a notification within my controller object when this notification is posted. Within this method I tell the view that has just been selected that it is selected and tell it to redraw, which makes it shade itself grey.
The NSCollectionViewItem Subclass:
-(void)setSelected:(BOOL)flag {
[super setSelected:flag];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ASCollectionViewItemSetSelected"
object:nil
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:(ASListView *)self.view, #"view",
[NSNumber numberWithBool:flag], #"flag", nil]];}
The Controller Class (in the -(void)awakeFromNib Method):
//Register for selection changed notification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(selectionChanged:)
name:#"ASCollectionViewItemSetSelected"
object:nil];
And the -(void)selectionChanged:(NSNotification *)notification method:
- (void)selectionChanged:(NSNotification *)notification {
// * * Must get the selected item and set its properties accordingly
//Get the flag
NSNumber *flagNumber = [notification.userInfo objectForKey:#"flag"];
BOOL flag = flagNumber.boolValue;
//Get the view
ASListView *listView = [notification.userInfo objectForKey:#"view"];
//Set the view's selected property
[listView setIsSelected:flag];
[listView setNeedsDisplay:YES];
//Log for testing
NSLog(#"SelectionChanged to: %d on view: %#", flag, listView);}
The application that contains this code requires there to be no empty selection within the collection view at any time. This is where i get my problem. I've tried checking when a view's selection is changed and reselecting it if there is no selection, and manually selecting the views using NSCollectionView's
-(void)setSelectionIndexes:(NSIndexSet *)indexes
But there is always a situation which occurs that causes there to be an empty selection in the collection view.
So I was wondering if there is an easier way to prevent an empty selection occurring in an NSCollectionView? I see no checkbox in interface builder.
Thanks in advance!
Ben
Update
I ended up just subclassing my NSCollectionView, and overriding the - (void)mouseDown:(NSEvent *)theEvent method. I only then sent the method [super mouseDown:theEvent]; if the click was in one of the subviews. Code:
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint clickPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
int i = 0;
for (NSView *view in self.subviews) {
if (NSPointInRect(clickPoint, view.frame)) {
//Click is in rect
i = 1;
}
}
//The click wasnt in any of the rects
if (i != 0) {
[super mouseDown:theEvent];
}}
I ended up just subclassing my NSCollectionView, and overriding the - (void)mouseDown:(NSEvent *)theEvent method. I only then sent the method [super mouseDown:theEvent]; if the click was in one of the subviews. Code:
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint clickPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
int i = 0;
for (NSView *view in self.subviews) {
if (NSPointInRect(clickPoint, view.frame)) {
//Click is in rect
i = 1;
}
}
//The click wasnt in any of the rects
if (i != 0) {
[super mouseDown:theEvent];
}}
I also wanted to avoid empty selection in my collection view.
The way I did it is also by subclassing, but I overrode -hitTest: instead of -mouseDown: to return nil in case the click wasn't on an item :
-(NSView *)hitTest:(NSPoint)aPoint {
// convert aPoint in self coordinate system
NSPoint localPoint = [self convertPoint:aPoint fromView:[self superview]];
// get the item count
NSUInteger itemCount = [[self content] count];
for(NSUInteger itemIndex = 0; itemIndex < itemCount; itemIndex += 1) {
// test the point in each item frame
NSRect itemFrame = [self frameForItemAtIndex:itemIndex];
if(NSPointInRect(localPoint, itemFrame)) {
return [[self itemAtIndex:itemIndex] view];
}
}
// not on an item
return nil;
}
Although I'm late to this thread, I thought I'd just chime in because I've had the same problem recently. I got around it using the following line of code:
[_collectionView setValue:#NO forKey:#"avoidsEmptySelection"];
There is one caveat: the avoidsEmptySelection property is not part of the official API although I think that it's pretty safe to assume that its the type of property that will stick around for a while.

How to alloc initWithCoder or initWithFrame

#import "DotAnimation.h"
#implementation DotAnimation
DoodlePad* dp;
-(void)setupView {
Ball = CGRectMake(10, 10, 10, 10);
[NSTimer scheduledTimerWithTimeInterval:(1.0f / 30.0f) target:self selector:#selector(traceBall) userInfo:nil repeats:YES];
}
-(id)initWithCoder:(NSCoder *)aDecoder {
if (self == [super initWithCoder:aDecoder]){
[self setupView];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setupView];
}
return self;
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.*/
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [[UIColor yellowColor] CGColor]);
CGContextFillEllipseInRect(context, Ball);
}
-(void)traceBall{
vector<CGPoint>::iterator L = dp.doodlePoints->begin();
while(L != dp.doodlePoints->end()){
Ball.origin.x += (*L).x;
Ball.origin.y += (*L).y;
++L;
}
[self setNeedsDisplay];
}
- (void)dealloc
{
[super dealloc];
}
#end
I have this Animation I am trying to use in another file.
So i figure i do something like
trace = [[DotAnimation alloc]initWithCoder];
or
trace = [[DotAnimation alloc]initWithFrame];
I am unsure which one to use or if i am even writing this correctly.
I want to be able to use:
while(k != dp.doodlePoints->end()){
Ball.origin.x += (*k).x;
Ball.origin.y += (*k).y;
++k;
}
In another file but I don't know how to call Ball.origin from DotAnimation
Also it would be great if you could link me to good information on understanding this.
initWithCoder is called by the framework when you embed an object into a XIB. Like a ViewController for example. So you should not call it by yourself.
Calling initWithFrame could be a better solution if you want to set the frame (like that seems to be done here). But you may give a frame, and add a subview created by yourself.
if you don't want to create the subview by yourself, then it's initWithNibName:bundle: that you may call/override. Creating the XIB that contains the view. But you will still need to add that view as subview to the main view that is showing that animated view.
initWithCoder: is something you would call yourself when restoring from an archive; it isn't appropriate for something which appears to be a view object. As it is only used for simple setup code, you can remove it.
Also, animating by changing the "Ball" object from another object isn't really consistent with Cocoa view programming. You should take a look at "Introduction to View Programming Guide for Cocoa" in the documentation, and work from there.

viewDidLoad in NSViewController?

On the iPhone I use UIViewController's viewDidLoad to run code to set up the view.
How can I do that with NSViewController?
I've tried loadView but it doesn't work...
I figured it out within minutes of posting my comment. Adding my finding as an answer because it is an example which is missing in the docs. The below code will give you the viewDidLoad method that you want. Its so easy in a way that i wonder why Apple has not implemented it yet in OS X.
- (void)viewWillLoad {
if([NSViewController instancesRespondToSelector:#selector(viewWillLoad)]) {
[super viewWillLoad];
}
...
}
- (void)viewDidLoad {
if([NSViewController instancesRespondToSelector:#selector(viewWillLoad)]) {
[super viewDidLoad];
}
}
- (void)loadView {
BOOL ownImp = ![NSViewController instancesRespondToSelector:#selector(viewWillLoad)];
if(ownImp) {
[self viewWillLoad];
}
[super loadView];
if(ownImp) {
[self viewDidLoad];
}
}
Original source: http://www.cocoabuilder.com/archive/cocoa/195802-garbage-collection-leaks-and-drains.html
As of OS X 10.10, viewDidLoad is available and supported on NSViewController.
Prior to that, you had to go by this nugget in Snow Leopards' release notes:
Advice for People who Are Looking for -viewWillLoad and -viewDidLoad Methods in NSViewController
Even though NSWindowController has -windowWillLoad and -windowDidLoad methods for you to override the NSViewController class introduced in Mac OS 10.5 does not have corresponding -viewWillLoad and -viewDidLoad methods. You can override -[NSViewController loadView] to customize what happens immediately before or immediately after nib loading done by a view controller.
As of OSX 10.10 (Yosemite), there is now a -viewDidLoad, -viewWillAppear, -viewDidAppear, -viewWillDisappear in NSViewController. See WWDC 2014 - Storyboards and Controllers on OS X session for more info, to find out when each of them gets called, etc.
Here's the relevant bit from the 10.10 header docs about -viewDidLoad:
Called after the view has been loaded. For view controllers created in
code, this is after -loadView. For view controllers unarchived from a
nib, this is after the view is set. Default does nothing.
- (void)viewDidLoad NS_AVAILABLE_MAC(10_10);
why don't you try this:
- (void)awakeFromNib {
//setup code
NSLog(#"hello there");
}
It looks like in 10.10, viewDidLoad is now in NSViewController.
hmm actually I would also do this...
- (void)viewWillLoad {
if (! bool_viewwillload) {
// execute the code
bool_viewwillload = true;
}
}
- (void)viewDidLoad {
if (! bool_viewdidload) {
// execute the code
bool_viewdidload = true;
}
}
and then just make the load view like this
- (void)loadView {
[self viewWillLoad];
[super loadView];
[self viewDidLoad];
}