How do I optimise Cocoa drawing to ensure smooth scrolling - macos

I am drawing graphs on NSView using standard Cocoa drawing APIs. See image below. There are multiple graphs on the NSView which is in a scrollView. Each graph has around 1440 data points and scrolling performance struggles a bit because of redrawing that is being done.
Is there any way to ensure the graphics are only drawn once such that the image can be scrolled up and down smoothly ?
This same view is used to generate a PDF output file.
Given the drawing does not actually need to change unless the view is resized, and this does not happen, is there any way to prevent the view from redrawing itself during scrolling. Hopefully there is a simple switch to ensure the view draw itself once and keeps that in memory!?
The basic code is in the NSView subclass draw() function.
override func draw(_ dirtyRect: NSRect) {
drawAxis()
// Only draw the graph axis during live resize
if self.inLiveResize {
return
}
plot1()
plot2()
plot3()
...
}
func plot1(){
for value in plot1Data {
path = NSBezierPath()
if isFirst {
path?.move(to: value)
} else {
path?.line(to: value)
}
}
}

Apple has pretty comprehensive advice in their View Programming Guide: Optimizing View Drawing article.
It doesn't appear that you're taking even the minimal steps to avoid drawing what you don't have to. For example, what code you've shown doesn't check the dirtyRect to see if a given plot falls entirely outside of it and therefore doesn't need to be drawn.
As described in that article, though, you can often do even better using the getRectsBeingDrawn(_:count:) and/or needsToDraw(_:) methods.
The scroll view can, under some circumstances, save what's already been drawn so your view doesn't need to redraw it. See the release notes for Responsive Scrolling. One requirement of this, though, is that your view needs to be opaque. It needs to override isOpaque to return true. It's not enough to just claim to be opaque, though. Your view actually has to be opaque by drawing the entirety of the dirty rect on every call to draw(). You can fill the dirty rect with a background color before doing other drawing to satisfy this requirement.
Be sure the clip view's copiesOnScroll property is set to true, too. This can be done in IB (although it's presented as an attribute of the scroll view, there) or in code. It should be true by default.
Note that the overdraw that's part of Responsive Scrolling will happen incrementally during idle time. That will involve repeated calls to your view's draw() method. If you haven't optimized that to only draw the things that intersect with the dirty rect(s), then those calls are going to be slow/expensive. So, be sure to do that optimization.

Related

The internals of NSScrollView

When you gently scroll an NSScrollView the rectangle that Cocoa marks as dirty, and passes to drawRect, is often trivially small (perhaps as small as one or two pixels in height, for a vertical scroll view). The framework clearly already knows what the majority of the content is (because it's on screen) and where to redraw it (just the offset brought about by the scroll), so all it needs the developer to do is fill in the small rectangle that's about to appear. I was wondering what's happening behind the scenes to allow this to happen?
For example, if I wanted to implement my own super-smooth scroll view as a learning project, what kind of data would I be recording about the document view to enable me to just re-position - rather than redraw - the majority of it. Is Cocoa constantly generating images on background threads that it draws on screen when required, or is there something a bit more subtle going on?
There's lots going on. If you haven't already read it, you should read the Scroll View Programming Guide for Cocoa.
The copying of the existing rendering is accomplished by -[NSView scrollRect:by:]. It's only done if the NSClipView that's part of the NSScrollView architecture is set to copy-on-scroll (the copiesOnScroll property).
Also, there's "responsive scrolling". Since 10.9, if certain conditions are met, AppKit will speculatively render the document view beyond the visible rect so that, when the user scrolls, it can show the scrolled-in area without asking the document view to render.
You can set your views to be layer-backed. In that case, they are typically rendered to textures and composited by the window server. This means they don't necessarily have to re-draw to render in a new position. It's quite likely that responsive scrolling uses layers behind the scenes to hold the pre-rendered content.

How to animate horizontal bar with right cap, by UIView animation in iPhone

I got wiered but unsurprising animation from call UIView animation, I don't know if it works the same by using CALayer animation. I guess it is.
please see this picture: what I want is to animate a horizontal bar growing horizontally, what I do is:
Subclass a UIView, called UIViewBar, and in its drawRect:rect method, I draw the shape of rectangle and a right cap, it's done by
CGContextMoveToPoint A
CGcontextAddLineToPoint B
CGContextAddArcToPoint D
CGContextAddLineToPoint C
//then close the path and fill the context
Then I call this UIViewBar (initWithFrame:rect) in my UIViewController, and the shape is what I want, looks fine. BUT when I perform [UIView animation] say from original rect to rectGrew it does not perform nicely. Instead, it perform as the picture says.
Well, after thinking, it's not totally unreasonable because when the UIView horizontally stretches itself, it does not know the cap will not be changed. So is there another way to do this?
Actually, I'm very new to drawing, graphic, and animation. What I know is that (if wrong please kindly correct me):
If I want to "draw something", I should override the UIView's drawRect method, and this is done by initing the UIView, should not or better not to be called outside. Say I drew a rectangle in a UIView by CGContextFillRect.....
But what if I want to animate this rectangle? I cannot make animation based on Quartz2D, but from UIView animation or CALayer(deeper tech), so What I have to do is to make the rectangle itself as a seperate UIViewRect seperately, so I can call the [UIViewRect animation] method, AM I RIGHT ON THIS
If so, I have to make every rectangle that I want to animate to be UIView respectively. Doesn't it affect performance? Or it is just the way apple prefers to do ?
Any suggestion will be much helpful, GREAT thanks.
Wow, it's the first time that no answer posted at all! TO answer my question: it requires to consider animate custom property of CALayer. The custom property is the AB width (the width without cap width, that should work). If someone wants detailed code. Please google custom property animate in CALayer.

IOS: Animate UICollectionViewCells to slide when changing orientation

I'm looking to override my UICollectionViewFlowLayout to cause the cells to slide when changing positions due to an orientation change. What I have currently is 3 cells per row in portrait and 4 cells per row in landscape but the only animation I can get is the default fading in when I change orientations.
I've been looking at using performBatchUpdates but I'm pretty sure that's not where I'll find the answer.
I've also been looking into the layoutAttributesForElementInRect and layoutAttributesForItemAtIndexPath which I think is where I'll find the answer. I'm not sure if I'll need to create a new attribute for the cells (previousCenter maybe) and use that as the starting point for new animations or maybe use performBatchUpdates using what should be the new frame as the value to change.
For the record all animation questions I've found have been about how to change animations for insertion and deletion of items which is not what I'm wanting to do.
Try
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:( NSTimeInterval)duration {
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self.collectionView reloadData];
}
and then add whatever you need to "slide" it at that point.
If you are using flowLayout or a subclass of it, it should automatically reposition the cells for you.If you subclass layout you might need to adjust the layoutattributes you return to correspond to the rotation. The animation should be done for you though.

NSScrollView Zooming of subviews

Apologies for the noob question - coming from an iOS background I'm struggling a little with OSX.
The good news - I have an NSScrollView with a large NSView as it's documentView. I have been adjusting the bounds of the contentView to effectively zoom in on the documentView - and all works well with respect to anything I do in drawRect (of the documentView)
The not so good news - I have now added another NSView as a child of the large documentView and expected it to simply zoom just like it would in iOS land - but it doesn't. If anyone can help fill in the rather large gap in my understanding of all this, I'd be extremely grateful
Thanks.
[UPDATE] Fixed it myself - 'problem' was that autolayout (layout constraints) were enabled. Once I disabled them and set the autosizing appropriately then everything was ok. I guess I should learn about layout constraints...
I know this is very old but I just implemented mouse scroll zooming using the following after spending days trying to figure it out using various solutions posted by others, all of which had fundamental issues. As background I and using a CALayers in a NSView subclass with a large PDF building layout in the background and 100+ draggable CALayer objects overplayed on top of that.
The zooming is instant and smooth and everything scales perfectly with no pixellation that I was expecting from something called 'magnification'. I wasted many days on this.
override func scrollWheel(with event: NSEvent) {
guard event.modifierFlags.contains(.option) else {
super.scrollWheel(with: event)
return
}
let dy = event.deltaY
if dy != 0.0 {
let magnification = self.scrollView.magnification + dy/30
let point = self.scrollView.contentView.convert(event.locationInWindow, from: nil)
self.scrollView.setMagnification(magnification, centeredAt: point)
}
}
LOL, I had exactly the same problem. I lost like two days messing around with autolayout. After I read your update I went in and just added another NSBox to the view and it gets drawn correctly and zooms as well.
Though, does it work for NSImageViews as subviews as well?

Custom Draw in NSView without erasing the previous background

I am trying to plot few points dynamically on my Custom View using Quartz functions so that i get a complete graph. I handle drawing of lines inside the drawRect method of Custom View where I get the current context and draw the lines. But, they get erased when i try to draw a new line. I want to have those lines also visible along with the new ones drawn. Please let me know how to do this. I can't store all the points together and draw at the end. I want to continously update my view. Thanks in advance.
Add a method to your custom view:
- (BOOL) isOpaque { return YES; }
That will prevent the drawing of any views behind yours including the background.
Note, however, that on resize you'll need to redraw everything either way. A more proper solution would be to use off-screen image to draw into instead.
You could use CALayers: add a new child layer to the root each time you have new data, and draw to that layer. Your drawing code can remain the same: you just need to put in the code for creating and using layers, which is actually pretty easy.
See: http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004514

Resources