I have an NSTableView with 5 columns, each containing a stock NSTableCellView in the nib. (The stock cells have a text box and an optional image.) When populated, the table has around 50 rows. Everything displays fine, but scrolling performance is pretty bad. It looks like this is happening because every cell gets a drawRect: message for its full rect whenever the table scrolls. However, neither reloadData nor reloadDataForRowIndexes:ColumnIndexes: is getting called, so it's not that. It's not the contents of the cells, either: I tried commenting out all my code to just leave the default cell image and text for each cell, and performance is the same. While scrolling, none of the cells get updated. (I put a breakpoint in tableView:viewForTableColumn:row: to make sure.)
My implementation has the following delegate methods:
tableView:viewForTableColumn:row: in the delegate; this creates and populates new cells via makeViewWithIdentifier:owner:
numberOfRowsInTableView: in the data source; this returns a constant number
tableView:sortDescriptorsDidChange: in the data source
That's it! Not very complicated, and yet.
I feel like I'm missing something completely obvious. What could be causing these redraws?
EDIT: Come to think of it, several other applications (uTorrent, Xcode) seem to exhibit the same slow scrolling behavior. You can really see it if you look at CPU usage while scrolling. On the other hand, Activity Monitor has buttery-smooth scrolling that barely spikes the CPU at all. How do I get that in my app?
EDIT 2: I think I found my mistake. According to Apple:
In iOS apps, Core Animation is always enabled and every view is backed
by a layer. In OS X, apps must explicitly enable Core Animation
support by doing the following:
Link against the QuartzCore framework. (iOS apps must link against this framework only if they use Core Animation interfaces explicitly.)
Enable layer support for one or more of your NSView objects by doing one of the following:
In your nib files, use the View Effects inspector to enable layer support for your views. The inspector displays checkboxes for
the selected view and its subviews. It is recommended that you enable
layer support in the content view of your window whenever possible.
For views you create programmatically, call the view’s setWantsLayer: method and pass a value of YES to indicate that the
view should use layers.
Enabling layer support in one of the preceding ways creates a
layer-backed view. With a layer-backed view, the system takes
responsibility for creating the underlying layer object and for
keeping that layer updated. In OS X, it is also possible to create a
layer-hosting view, whereby your app actually creates and manages the
underlying layer object. (You cannot create layer-hosting views in
iOS.) For more information on how to create a layer-hosting view, see
“Layer Hosting Lets You Change the Layer Object in OS X.”
I'll add an answer as soon as I fix my performance issues. With a cursory pass, my scrolling is still bumpy, but my CPU usage has dropped from 70% to 10% while scrolling.
For the record... Edit 2 by the OP makes the world of difference.
In iOS apps, Core Animation is always enabled and every view is backed
by a layer. In OS X, apps must explicitly enable Core Animation
support by doing the following:
Link against the QuartzCore framework. (iOS apps must link against
this framework only if they use Core Animation interfaces explicitly.)
Enable layer support for one or more of your NSView objects by doing
one of the following:
In your nib files, use the View Effects inspector to enable layer
support for your views. The inspector displays checkboxes for the
selected view and its subviews. It is recommended that you enable
layer support in the content view of your window whenever possible.
For views you create programmatically, call the view’s setWantsLayer:
method and pass a value of YES to indicate that the view should use
layers. Enabling layer support in one of the preceding ways creates a
layer-backed view. With a layer-backed view, the system takes
responsibility for creating the underlying layer object and for
keeping that layer updated. In OS X, it is also possible to create a
layer-hosting view, whereby your app actually creates and manages the
underlying layer object. (You cannot create layer-hosting views in
iOS.) For more information on how to create a layer-hosting view, see
“Layer Hosting Lets You Change the Layer Object in OS X.”
Related
According to the documentation for NSView's drawRect:
If your app manages content using its layer object instead, use the updateLayer method to update your layer instead of overriding this method.
I have an NSView with subviews that are provided by the framework, and they all draw using drawRect:. This framework-provided view is a subview of an NSView for which I require a layer. Because my framework-provided view is a descendant of a layer-backed view, drawRect: isn't usually called, especially in cases where the window is made active or inactive (the view needs to update to reflect its (in)active state).
Of course if I make my containing view not layer backed, updates occur when the window is made active or inactive.
Without modifying the framework into a custom fork, what's the best avenue for making sure drawRect: occurs when needed in my framework-provided view?
Thanks.
Edit 25-Aug-2018:
It looks like the trick is to set one of the views in the hierarchy to, e.g., [view setCanDrawSubviewsIntoLayer:YES, which according to the documentation uses all of the subviews’ drawRect: to add their drawing to its own layer. However this seems to work only through 10.13, and is broken in the 10.14 beta. I'll continue to look for a potential API change, unless this is a 10.14 beta bug.
Since the issue is still unresolved, it's not really answered yet.
Layer-backed views which don't override -wantsUpdateLayer to return true still draw themselves using -drawRect:. The bit of documentation you quoted is using "should" to mean "should, for best performance,". It's not required, it's just recommended.
Views don't generally redraw themselves just because the containing window has changed key or main status. You would have to mark them as needing display. Or the framework should be doing that.
I suspect the reason that it works when your view is not layer-backed is that you are marking your view as needing update. Since non-layer-backed views draw into the window's backing store using the painter model (back to front), if your view redraws itself then any subviews will have to redraw themselves on top of your view's drawing.
If the framework's views need to redraw when the window's key/main status changes, then they should be observing the relevant notifications and setting themselves as needing display. If they're not doing that, it's a framework bug. You can work around it by marking them as needing display yourself.
Several places in OS X (in this example, the Users & Groups pane in System Preferences) have circular image views that allow the user to either drag in an image, like in an editable NSImageView but also allow them to click to show a popover that allows various other choices of image sources.
I have checked the ImageKit framework, but the only thing I found similar is the image taking sheet.
How can I make use of this feature in my own Cocoa applications? I'd imagine it is implemented in some standard framework—but any pointers on implementing something like this would be quite appreciated.
You will have to go down the custom control root as this is not available as a stand alone control.
However you have all the prerequisites.
The circular image view
There a several ways to implement this. You could try using a standard Cocoa button and customise as needed. Although it might just be easier to build from scratch by subclassing NSView. This was you can avoid all the NSCell stuff. I would do the latter.
The popover
Roll your own master-details type view controller to be displayed as the popover's content. In the left have a NSTableView (the master), the right have a NSCollectionView (the details). Below the collection view add some buttons.
I have an NSPageController in book mode with two pages and each contains an NSTableView. If I start my application and resize it vertically and then swipe to the other page the snapshot used in the swipe animation is of the NSTableView before the resize. This view swiping in only covers part of the previous view and this looks terrible. Is it possible to get the NSPageController to invalidate the snapshots when the view is resized?
The PictureSwiper example does exactly this by setting the frame of the view whenever the window is resized. However, you need to have your layout/resize constraints set. Whenever the view size changes, the constraints will cause NSPageController's view objects to be resized as well. If you're doing something unique with your view layout/size, you will need to manually resize as in the example linked.
Also, snaphots are generated on-the-fly. From the arrangedObjects method documentation of NSPageController:
The delegate will be asked for snapshots as they are needed.
And this is useful to keep in mind:
When using the book mode, if pageController.view is layer backed, live layers are used during transition instead of snapshots.
In iOS, you have a concept of View Containment, is there such things in OSX?
Basically I want to create multiple nsviewcontroller each managing a specific view. I'd have a MasterViewController with a menu on the left (like ITunes), each time the user click on an item on the left, it would load the correct nsviewcontroller to display it's view.
Any tips to achieve what I need is appreciated
Thanks,
As of OSX 10.10 there is, watch Storyboards and Controllers on OS X.
Comment.
NSViewController did basically nothing (other that load NIBs) for years, I'm glad to see that it finally got from attention. Certain people in the Cocoa crowd here have a snotty attitude about the view controller programming style; I've asked questions like this before and had the "are you a iOS newbie coming to Cocoa" response. That's something that I never understood, it's a great model for containment, and reuse.
The main difference between OS X and iOS is that on an iOS device you have only one "window". On OS X there are desktops that can contain many windows that you can view and interact with at the same time.
In general, it sounds like you are trying to create an NSWindow that contains a single-column NSTableView for your list of choices on the left, and some other view that will display the detail of the selection on the right. It's common to place these within a vertical NSSplitView so the user can adjust their relative widths, but they could also stand on their own, as two separate subviews within the window's main view.
You typically use an NSArrayController to manage the list contents and track which particular item is selected. For your detail view on the right, you would use a single NSView with NSControl subviews that display values bound to the array controller's selected object.
If the data structure varies among your objects, swap in or show/hide various subviews as needed for the different types of data the particular selected object represents. You can use the "Conditionally Sets Hidden" binding option to automatically hide controls for which there is no applicable keyed value.
Alternatively, if there's a fixed number of objects in your list and their structures are all quite different from one another, then you may wish to use a tabless NSTabView that has a separate tab with its own custom view for each of your objects. Observe when the selection changes in your list, and select the appropriate tab accordingly.
In my drawing app I have a switch called "Snap to grid" -- When its state is changed I need drawing in my canvas view to honor that change. At present, the switch is a subview of a UIView so that the text label and the switch can travel together during a device orientation change.
I'm trying to decide what is the best way of communicating the state change. My thought train has been comparing this to using jQuery selector, like a $("#switch") -- in essence, when the view controller has instantiated the subviews (and subviews of those subviews), how do you get a pointer to the specific subview you're looking for?
I should add that my views are created in the Interface Builder portion of Xcode, so they are loaded from a .nib file rather than me creating them (which obviously would give me a chance to hold on to a pointer to the subviews I need).