Cocoa - efficient view drawing - cocoa

in my program, I'm implementing a custom view, a bit like a table view. To do so, I have subclassed NSView. Now my question is, what's the most efficient way to draw all the table view cells. Should I just use NSViews or possibly something else, like CALayers?
Thanks!
P.S.: This is on Mac OS X, not on iOS.

Based on your question and comment, I propose two alternatives:
NSCollectionView / NSCollectionViewItem - This is useful only if all of your "cells" (instances of your prototype view) are the same dimensions. That is, you can't have one that's wider or taller than the others (or narrower or shorter). This is highly efficient and a ready part of AppKit. Even with a single column and n rows, it works like a charm.
Roll Your Own - This is harder but gives you flexibility. Much like NSCollectionView / NSCollectionViewItem, you'd have a view that serves as the container and you'd ideally have a view you reuse to draw the various "items" it's showing. Using the same view to set its represented object and "stamp" it into place (pose it and draw it), you can roll through your entire collection in one go then use that same view as the live, active view for whatever selected, focused item you have. Even faster: roll through and cache the images and sizes of each item with your reusable item view and draw all from the cache except the selected item (which would use a live, real view posed in its proper location, updating the cached image of itself as its contents change for when it's not selected). Faster still: 1 live view and 1 "for caching" view, and draw only the computed rects of the cached images that intersect the visible rect (sans the "live" / selected view). Note: the caching will have to be re-done each time the container's frame's width changes, since presumably shrinking horizontally means all items grow vertically. If you can, take advantage of NSOperation / NSOperationQueue to handle the caching in the background, only flagging for re-display when all cached items 0 - n (where n is the highest-indexed item intersecting the visible rect) are available.
I use something very close to the latter in one of my own applications, where the "item" is an entry with varying-length text. I don't employ all the tactics I mentioned in my own solution but most and the performance increase is very satisfying. :-)
Hope this helps.

Related

What could prevent an NSView.layer to renderInContext:?

I've been working hard, searching the internet on that problem for 3 days and I'm now running out of ressource.
Currently porting an iOS app to MacOS (deployment 10.11). The problem:
I have a view hierarchy as below:
NSScrollview
documentView
grouping view
tiling view one
array of NSImageView (each one being a tile)
tiling view two
array of NSImageView (each one being a tile)
The two tiling views are overlaying completely, depending of UI one may be hidden or the second one has opacity set below 1.0 to blend the two tiled view.
Because of opacity requirement, as well as performance, views are CAlayer backed. This is done from IB where the to NSScrollview is checked for Core Animation. Thereoff, all the view tree is (implicitly) layer backed.
Works as expected, scroll, magnify, etc..
Then I need to make an image out of the document view to generate an SCNMaterial content (3D view).
On iOS the documentView renderInContext works as expected, and allows an image to be created.
On Appkit the context stay transparent, so is the image, a valid object while as if clearColor.
If the documentView.canDrawSubviewsIntoLayer is set at creation, the view tree renders OK. This can't be the solution since it prevents opacity setting to work.
Even when one tiling view is hidden (no opacity compositing) rendering fails.
I read that some kinds of views are not rendered. I don't use them. There are no filters, no masks, beside a default masksToBounds setting on all the view tree. I don't know why and where it is set. I tried to unset it on all the views at creation with no success. It is set again somehow, on the grouping view below the documentView. This may be the problem but why this property is out of my control ?
Alternative way to get view tree rendering, bitmapImageRepForCachingDisplayInRect: / cacheDisplayInRect:toBitmapImageRep: works the same : ok with canDrawSubviewsIntoLayer, KO otherwise. Apple code examples to make a texture out of a view are using either one of the two methods.
There are plenty of posts, mainly on SO, complaining about CALayer renderInContext and code to custom render a layer tree. Nevertheless, most are quite old and now there must be a simple standard way to achieve it.
Edit: among other attempts, I tried to set each view wantsLayer, with no success.
Well, as often when you post a request for help, you finally find the answer by yourself… Here it is:
Given the view tree as listed in the question, I achieved to render it by setting canDrawSubviewsIntoLayer on each tiling view. This way, the opacity compositing between layers is working, AND the views are rendered.
I post this as an answer, because it solves the problem.
As of WHY this works, here is my guess, but this is not a authorised answer: Each NSImageView tile, subviews of tiling view, have their origin set to their frame. This is not a transform on the layer but a position of the frame in the coordinates of the tiling view. This is a difference with the iOS code version where the tiles are positioned by a transform. I'm going to test further, and see if using a transform rather that a frame origin makes a difference to renderInContext.
Edit: after more testing it appears that the iOS version has a transform AND an offset to the tile.
So the only clue is that the layer system get lost when rendering in context on OSX when some subview have an offset ??
Summary:
The key point to the solution is to find the right layer where to set canDrawSubviewsIntoLayer

how to translate and scale a NSImage?

I have built so far an application that allows the user to drag and drop images onto a NSImageView. However, I want to be able to move these images by simply clicking on any image and hold down the mouse button to move it's location.
How can I manipulate NSImageView to translate/scale after setting the images down? Is that possible? I've read about the NSAffineTransform, but it seems like that is moving the images before creating the image itself. I already have the images on the canvas, and simply want to click and hold the image and move it with my mouse. Please help anyone!
There are two sides to this.
NSImage is the model object, which you might want to display in different ways, save to disk/archive, etc. If you want to actually change the model (scaling, rotating, etc.), implying a permanent change, then you are going to probably want to look at NSAffineTransform, Quartz drawing, etc.
But you probably didn't mean that. Instead you probably are interested in NSImageView, which is a view object, displaying the contents of the NSImage model object using whatever display attributes are desired. If you only want to change how an image is displayed, not what the actual bytes in the image are, then you are going to manipulate the NSImageView at run-time. You can use NSAffineTransform here as well, but it's somewhat uncommon (and usually unnecessary).
The key thing to note that is the NSImageView inherits from NSView, so you have all its power at your disposal. Take a look at certain methods, such as:
-setFrameSize: - useful for changing the view size, and thus the image display scale
-setFrameOrigin: - useful for changing the view position, and thus the apparent image position
Note again that these have nothing to do with images per se, and apply to all Cocoa views. You may want to take a look at a book like Cocoa Programming for Mac OS X to get you past the basics. (You can then do more interesting things, like rotation, animation, etc.)

How to scroll efficiently in an NSScrollview with huge amount of data?

I'm scrolling in a huge set of data points (CGPoints in a custom view, like a graph) and its obviously slow and laggy. Is there a common way to load and draw only the chunk of data I need to display? Does [[[myScrollview] documentView] documentVisibleRect] the trick?
I also want to zoom in and out and therefore change the data point's detail.
Thanks for any tips.
How you do this is completely up to you. It's your custom view, you can draw whatever you like. Your view controller should keep track of the zoom level and current location and your view should just draw that part of the data.
There's nothing magic that's going to make this easier, you have to program it yourself.

How to implement a timeline custom control with cocoa?

My Cocoa application collects events (instances of NSManagedObject) that need to be displayed in a timeline. My initial approach was to use an existing Javascript based widget (I tried using Simile Timeline and Timeglider) and display the timeline using a WebView control. This works in principle, however unfortunately both these widgets do not handle BC dates very well, which is an important requirement for my app.
The events in my app have date ranges from 500.000BC up to recent dates. Event dates are only expressed with a year. Their day, month and time attributes are irrelevant.
After discarding the Javascript approach, I remain with the option to display the timeline using a custom Cocoa control. As I found none suitable, I will have to develop that myself.
This would be my first custom Cocoa control and after thinking about this for a while I've come up with the following rough design:
I need a custom control to render the actual time line. This control is probably based on an NSView. This control should calculate its size based on the number of tick marks on the time line multiplied by the width (pixels) between each mark. For example the timeline is made up of centuries, each century 100 pixels wide. A time line of events between 10.000BC and 5.000BC would then be 5000 pixels wide (10000 - 5000 = 5000 years, equals 50 centuries).
I need a ScrollView to wrap the timeline to allow it to support scrolling behaviour. There's only need for scrolling horizontally.
I need something to represent an actual event. I'm thinking of using existing controls for this, probably the standard round button and a label wrapped together as a single control.
I need a custom control to render a tick mark on the time line.
Taking this as the basic design for my time line component in Cocoa, would that work or am I completely missing the point?
The basic approach sounds fine.
Apple has a good example of creating a custom NSView called "TreeView". It's a good sample to understand.
https://developer.apple.com/library/mac/#samplecode/TreeView/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010131
“TreeView” presents an example of a creating an entirely new custom
view from scratch (by directly subclassing NSView). Its implementation
illustrates many of the considerations involved in creating a custom
view, including issues of content layout, drawing, handling user
interaction, and providing Accessibility support.
Another thing you may want to consider is zoom in and out. If you have a long timeline, I imagine you may want to zoom out and then zoom in on a cluster of activity. If you have one event in 10k BC and then a cluster of events much later, the user could scroll through tons of empty space trying to find events. Another approach would be to have a mini timeline above that's fit to/static size which is sort of like an index with lines showing activity points - then clicking on that would auto scroll to that point. That may be a nice to have depending on your data.
Some thoughts:
For something this custom drawn, you'll want to override drawRect to draw your lines and layout your subControls.
If you're drawing your background or any part of the views, ensure you enable layer backed views:
[self setWantsLayer:YES];
If you can, as you noted, try leverage existing controls that you add and layout. In my custom controls, I maintained data structures independent of the views/controls that represented the state of all the objects. The in drawRect, I detected the view changing and I called my layoutSubviews function. My layoutSubViews function would do the math from my data structures and create or move the frame of existing controls. That worked well for resize and zooming. If you zoom, your labels ad markers will need to react well to being zoomed really small - perhaps text drops out at some point etc...
if ([self dataSource] &&
!NSEqualRects(_prevRect, [self bounds]))
{
// layoutViews is my custom function that worked over the data structures
// and moved the frame
[self layoutViews];
}
_prevRect = [self bounds];
Hope that helps.

NSView leaves artifacts on another NSView when the first is moved across the second

I have an NSView subclass that can be dragged around in its superview. I move the views by calling NSView's setFrameOrigin and setFrameRotation methods in my mouseDragged event handler. The views are both moved and rotated with each call.
I have multiple instances of these views contained by a single superview. The problem I'm having is that, as one view is dragged over another, it leaves artifacts behind on the view it's eclipsing. I recorded a short video of this in action. Unfortunately, due to the video compression the artifacts aren't very visible.
I strongly suspect that this is related to the simultaneous translation and rotation. Quartz Debug reveals that a rectangle of the occluding (or occluded) view is updated as another view is dragged across it (video here); somehow this rectangle is getting miscalculated by the drawing engine, so part of the view that should be redrawn isn't.
The kicker is I have no idea how to fix this. I can't find any way to manually specify the update rect in the docs, nor am I sure that's what needs to happen. Any ideas? Thanks!
You might also consider using CALayers instead of views. Unlike views, layers are intended to be stacked with their siblings.
For a possible least-effort solution, try making the views layer-backed; it may or may not solve this problem, but it's worth a try.
Views aren't really designed to be stacked in an interactive fashion. Can be done, but edge cases abound.
Generally, for this kind of thing you would use a Cell like infrastructure if you want to do in-view dragging (See the Sketch example) and you would use the drag-n-drop infrastructure if you want to drag between views or windows (or apps).
If you really want to drag a transformed view over the top, you'll need to invalidate a rectangle of the view underneath the view being dragged. The rectangle will need to be bigger by a few pixels than the total area (unrotated/untransformed) that is obscured by the view being dragged. The artifacts are, effectively, caused by rounding error; diagonal lines are just an estimate on a raster drawing system.
See the method:
- (void)setNeedsDisplayInRect:(NSRect)invalidRect;

Resources