IBAction not triggering NSView drawing - cocoa

I'm missing something fundamental about NSView. I have a Cocoa Application with an Objective C class named DataSource that is just a regular class, it's not in the nib. The data source has a single instance variable, an NSColor *, and it has a getter and setter.
The view class instantiates the DataSource in awakeFromNib:
- (void)awakeFromNib{
NSLog(#"awakeFromNib");
ds = [[DataSource alloc] init];
}
and then queries the DataSource for the color to use in drawRect. It works fine. I also implement
- (void)mouseDown:(NSEvent *) anEvent;
in the view class, change the color of the DataSource, and then call
[self setNeedsDisplay:YES];
and it also works as I expect when I click in the custom view.
But if I hook up a button in the nib, wired to this IBAction in the view class:
- (IBAction)buttonPushed:(id) sender {
NSLog(#"buttonPushed");
[ds setData:[NSColor cyanColor]];
[self setNeedsDisplay:YES];
}
the data source updates, but drawRect is never called, despite setNeedsDisplay. In my more complicated version, if I click in the view (in a way that doesn't change the color), I will then get the update (caused by the button). Something is delaying drawing. How can I fix this?
Update: There is no controller and there are no outlets. The NSView subclass contains buttonPushed. The data source updates immediately upon button push, but drawing is delayed, despite calling setNeedsDisplay:YES from the view class. Drawing is delayed indefinitely, unless something else happens to trigger it.

Where is the IBAction located? Are you using some view controller? Is the NSView an outlet in that controller?

Related

Knowing when NSPopoverTouchBarItem will show its collapsed view

I have an NSPopoverTouchBarItem in my Touch Bar, created in Interface Builder.
The popover has a custom NSView inside it, and want to load data in it only when the view is activated, but I can't find any way to recognize when the contained NSTouchBar or NSView becomes visible.
According to docs, NSTouchBarDelegate does not have any delegate methods for the view appearing, either.
Which class should I subclass, or should I be monitoring viewWillDraw on my custom NSView and set up some hacky scheme?
The docs were not too clear, but subclassing NSPopoverTouchBarItem gives you -(void)showPopover:(id)sender and -(void)dismissPopover:(id)sender.
You can then define a delegate method to tell the parent class that this popover did show.
-(void)showPopover:(id)sender {
[super showPopover:sender];
[self.delegate touchPopoverDidShow];
}

Click in view of NSCollectionViewItem

I'm new to Cocoa dev, so many concepts of it are not clear to me...
I'm trying to build a simple app which will use Flickr API to retrieve user photosets and show them in a NSCollectionView, by clicking them, will start to download the photos of the photo set.
I'm using Xcode 5.0.1 with latest SDK which is 10.9
After reading some articles about how to use binding to deal with NSCollectionView, I'm now facing another problem regarding handling events in NSCollectionViewItem.
Per I understanding, mouse events can be easily handled by implement
-(void) mouseDown:(NSEvent *)theEvent
In a NSView subclass, say
#interface MyViewController : NSView {
}
And assign the view custom class to the subclass I made (MyViewController) in InterfaceBuilder.
Now, I have no problem to do as above, and the mousedown did handled as expect in most of widgets.
The problem is, I have a NSCollectionViewItem subclass as below:
#interface MyItemController : NSCollectionViewItem {
}
I'm trying to implement mousedown method there, this class was set to as File's Owner in a separated nib file. And the view will be automatically load when the NSCollectionView loaded.
Now, MyItemController cannot be as customer class in the view object in IB which is obviously because of it is not a NSView subclass but a NSCollectionViewItem subclass.
If I write a subclass of NSView and make the custom class of view object, I can get the mousedown.
However, I cannot get the representedObject and index of NSMutableArray in this approach and they are the essential information I need.
So my question is, what is the right way to deal with mouse events view of NSCollectionViewItem?
My code in GitHub here:
https://github.com/jasonlu/flickerBackupTool
Thanks!
UPDATE
I found a approach to solve this problem is by subclassing NSView and implement mousedown and use super, subviews to get and index and the array itself
- (void)mouseDown:(NSEvent *)theEvent {
NSCollectionView *myCollectionView = (NSCollectionView *)[self superview];
NSInteger index = [[myCollectionView subviews] indexOfObject:self];
NSLog(#"collection view super view: %#",myCollectionView);
NSLog(#"collection index: %ld",index);
NSLog(#"array: %#", [[myCollectionView content] objectAtIndex:index]);
}
It seems work, but I'm not sue if this is the best practice, it looks like depends on view too much and took a long way to reach the array.
I wouldn't bet that NSCollectionView always creates all subviews (subviews which are far away from the viewing area might be delayed and/or reused). Therefore, I wouldn't rely upon subview searching.
Overload NSViewController to create an NSView so that the representedObject assigned to the NSViewController is accessible from the NSView. From there you could search the actual content for index determination.
Overloading NSCollectionView and recording the actual index during view creation would probably not work well because a deleted item probably doesNot re-create any views.

Make compiler aware of NSView subclass methods when building ViewController's view programmatically

I have created a subclass of NSView that allows me to easily change the background color via the method - (void)setBackgroundColor:(NSColor *)aBackgroundColor.
I want my view controller's main view to be this subclass, so I initiate it with self.view = [[BetterNSView alloc] initWithFrame....
On the next line, I try to set its background color: [self.view setBackgroundColor:[NSColor greenColor]]. But the compiler complains, saying that NSView doesn't have the method setBackgroundColor.
To solve this, I need to use typecasting: [(BetterNSView *)self.view setBackgroundColor:[NSColor greenColor]];. That works. But I'd like to not need the typecasting.
How do I tell the compiler that the view property of the view controller is the BetterNSView subclass? Remember, I'm not using IB.
You have two reasonable choices.
Make an accessor on your view controller with the right type that just passes through to the view property.
Deal with the typecast.
Really, this is one of this situations where typecasting is okay.

Cocoa: setters in an NSViewController view

I'm using an NSViewController class with a single view in it to display a progress indicator bar and some text fields. I'm trying to use progressIndicator setMaxValue:and theTextField setStringValue: but neither of these are doing anything.
I've done this before and I've checked and rechecked, it's fairly straightforward, the fact that it's not working makes me think that it has to do with the fact that the class is NSViewController. Which is why I tried
Timers *aTimer = [[Timers alloc] init];
[aTimer.timerNameLabel setStringValue:#"name"];
[aTimer.progressIndicator setMaxValue:x];
in the app delegate which is an NSObject class, but that didn't work either.
I've tried looking around the NSViewController documentation but I can't find anything that says it can't set those values so I don't know what's happening. What am I doing wrong?
You probably want to use -initWithNibName:bundle: instead of a regular init to initialize your custom nib.
EDIT: It seemed the problem was due to the view not being queried before getting other objects. By calling [myController view] you actually load the nib, which isn't done automatically when you initialize the view controller. So before you can use any element of the view, you need to call [myController view]

Calling setNeedsDisplay:YES on layer-hosting view does not redraw the view

I have a layer-hosting view set up like this in a custom NSView subclass:
[self setLayer:rootLayer];
[self setWantsLayer:YES];
I add all the sublayers to the layer tree after I called setNeedsDisplay on each sublayer. Each layer's content is provided by a drawLayer:inContext method of my layer's delegate.
Here is my problem:
After initializing my view the view gets draw correctly. However, when the model has changed and I call [myCustomView setNeedsDisplay:YES]; from my view controller the drawLayer:inContext is not called.
I am confused now how to update the view:
Do I have to call the setNeedsDisplay method on each CALayer in the layer tree?
Should not the call of setNeedsDisplay:YES on the layer-hosting view itself trigger the redraw of the whole layer tree?
Thanks for your help.
Edit
I have found something in the NSView Class reference
A layer-backed view is a view that is backed by a Core Animation layer. Any drawing done by the view is the cached in the backing layer. You configured a layer-backed view by simply invoking setWantsLayer: with a value of YES. The view class will automatically create the a backing layer for you, and you use the view class’s drawing mechanisms. When using layer-backed views you should never interact directly with the layer.
A layer-hosting view is a view that contains a Core Animation layer that you intend to manipulate directly. You create a layer-hosting view by instantiating an instance of a Core Animation layer class and setting that layer using the view’s setLayer: method. After doing so, you then invoke setWantsLayer: with a value of YES. When using a layer-hosting view you should not rely on the view for drawing, nor should you add subviews to the layer-hosting view.
link to documentation
In my case I have a layer-hosting view. So does that indeed mean that I have to trigger the redraw manually? Should I implement a pseudo drawRect method in the custom NSView to call the appropriate setNeedsDisplay on the CALayers that changed?
After further research in Apple's sample code of a kiosk-style menu I found out that if you are using a layer-hosting view, you have to take care of the screen updates which are neccessary due to model changes yourself. Calling setNeedsDisplay:YES on the NSView will not do anything.
So what one has to do if one has to update a view one should write a method like reloadData and in it one should call setNeedsDisplayon each CALayer that needs a refresh. I am still not sure if a call to this method on the root layer will propagate through all the children layers but I do not think so.
I solved the problem now by calling setNeedsDisplay on the individual CALayers that needed recaching. It works without problems.
There is also an oft-used practice of having an empty "drawrect", a la -(void) drawRect:(NSRect)dirtyRect {} to help coerce things into drawing, i believe via good ole view.needsDisplay = YES;.
and it should be noted.. that what is indeed happening is that - by saying your NSView *view; is layer.delegate = view; causes the layer to be drawn when [layer setNeedsDisplay]; is called.... via - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {...}..
along the same vein... when saying layer.layoutManager = view... subsequent demands that [layer setNeedsLayout]; will be fulfilled only when the - (void) layoutSublayersOfLayer:(CALayer *)layer {..} method is implemented..
These vital concepts are glossed over / strewn about in Apple's docs... and they are really so pivotal to making absolutely anything work at all.
You can automatically delegate the setNeedsDispay: by changing the redraw policy of the view. You have to assign NSViewLayerContentsRedrawOnSetNeedsDisplay to the property layerContentsRedrawPolicy (see https://developer.apple.com/documentation/appkit/nsview/1483514-layercontentsredrawpolicy). This will trigger redrawing of the layer when you send setNeedsDisplay: to the view:
[self setLayer:rootLayer];
[self setWantsLayer:YES];
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
or in Swift:
layer = rootLayer
wantsLayer = true
layerContentsRedrawPolicy = .onSetNeedsDisplay

Resources