How is memory handled with a UIView subclass inside UIScrollView - memory-management

I have a variable sized UIView with a drawRect containing CoreGraphics custom drawing code. The view was originally quite small but has the potential to become very wide and is placed inside a UIScrollView to enable the user to scroll through the view.
At the moment the view is created to be as wide as it needs to be to fit the entire drawing on and the CoreGraphics code draws across the entire view in drawRect.
- (void)drawRect:(CGRect)rect {
// Only try and draw if the frame size is greater than zero and the graph has some range
if ((self.frame.size.width>0) && (self.frame.size.height>0) && ((maxX - minX)>0)) {
// Get the current drawing context
CGContextRef context = UIGraphicsGetCurrentContext();
// Save the state of the graphics context
CGContextSaveGState(context);
// Set the line style
CGContextSetRGBStrokeColor(context, 0.44, 0.58, 0.77, 1.0);
CGContextSetRGBFillColor(context, 0.44, 0.58, 0.77, 1.0);
CGContextSetLineWidth(context, 3.0);
CGContextSetLineDash(context, 0, NULL, 0);
// Draw the graph
[self drawGraphInContext:context];
// Restore the graphics context
CGContextRestoreGState(context);
}
}
I can make this more efficient by:
Doing bounding calcs with the rect parameter in the drawRect and only drawing stuff that is currently visible.
Creating a smaller UIView (say 2x the width of the scrollView frame) and as the scroll position moves, redraw the image and re-centre the scrollView.
Before I go to the effort of doing either of these things, I would like to understand if it is worth the effort and whether I could run into problems later on if the UIView becomes very wide. So far performance is not an issue, but I am worried that memory might be.
Specifically, does the scrollView limit the amount of memory used by the view to the visible area? is the graphics context created the size of the entire view, the visible area or the size of the rect parameter? Are drawing operations outside the visible area simply ignored by CoreGraphics?
Any help and best practice advice would be appreciated.

IMO, it all depends on the rect argument to the call:
- (void)drawRect:(CGRect)rect
and as far as I know, this will be set to the contentSize of your scroll view. Simply, the context and the memory used for it will be related to that rect value.
You might trying doing your own "clipping" of the area you draw in by taking into account the frame of the scroll view.
Better yet, you could have a look at a sample by Apple about tiling content in a UIScrollView. It is sample 3.

Related

How do I optimise Cocoa drawing to ensure smooth scrolling

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.

NSView free-angle rotation

I have NSView container (with NSImageView and some other custom subviews). How to set its rotation properly? I tried to set angle through setFrameRotation: and set rotation matrix in views layer. But in these cases subview image becomes downscaled and clipped.
Update:
If I set rotation via [myView setFrameRotation: angle]:
almost fine, except text frame (drawing via [NSString drawAtPoint:...] and rotation anchor is at left-bottom corner (I want at bottom-center, [myView setFrameOrigin:...] does nothing)
If I set rotation via myView.layer.transform = CATransform3DMakeRotation (angle, 0, 0, 1):
frame bound remains unrotated and clips subviews (but this approach is more suitable for view container)

Unable to dynamically resize NSImageView

I have an NSImageView that encloses a dynamically generated NSImage. When I change the image displayed, I would like to dynamically resize the NSImageView so that it precisely wraps the new image, and also have the enclosing window resize so that the space between the NSImageView and every other part of the window remains constant. (Note that the image view's scaling is set to none, as I want its image to always be shown at its physical size.) To illustrate, suppose I begin with a small image in my image view. If I replace it with a large image, I wish for both the NSImageView and enclosing window to resize to accommodate it, without affecting the sizing or spacing of any other element.
Currently, I call the following method whenever the magnification level is changed via the stepper or associated text field. Though regenerating the image and loading it into the NSImageView works fine, resizing the NSImageView and enclosing window do not.
- (void) updateMagnification:(NSUInteger)newMagnification {
// Keep values of stepper and associated text field synchronized.
[self.magnificationStepper setIntegerValue:newMagnification];
[self.magnificationTextField setIntegerValue:newMagnification];
// Regenerate image based on newMagnification and display in image view.
[self.qrGenerator generateWithBlockPixelWidth:newMagnification];
self.imageView.image = self.qrGenerator.image;
// Adjust frame size of image view.
NSLog(#"Old size: frame=%# image=%#", NSStringFromSize(self.imageView.frame.size), NSStringFromSize(self.imageView.image.size));
[self.imageView setFrameSize:NSMakeSize(self.imageView.image.size.width, self.imageView.image.size.height )];
NSLog(#"New size: frame=%# image=%#", NSStringFromSize(self.imageView.frame.size), NSStringFromSize(self.imageView.frame.size));
//[self.window setViewsNeedDisplay:YES];
//[self.imageView setNeedsDisplay:YES];
//[self.imageView.superview setNeedsDisplay:YES];
}
Regardless of whether I increase or decrease the magnification value, causing the image to grow smaller or larger, the size of both the NSImageView and window remains constant. The three setNeedsDisplay: calls that are commented out have no effect even if they're uncommented -- they were my attempt to determine if the problem was related to the controls not redrawing once their size was adjusted, but the calls had no effect. Curiously, my NSLog calls indicate that the imageView's frame does indeed take the requested size, for they yield this output:
2012-06-12 11:02:50.651 Presenter[4660:603] Old size: frame={422, 351} image={168, 168}
2012-06-12 11:02:50.651 Presenter[4660:603] New size: frame={168, 168} image={168, 168}
The actual display, of course, does not change.
Interestingly, changing the imageView's frame style to "none," either in Interface Builder or programatically with [self.imageView setImageFrameStyle:NSImageFrameNone], gives me behaviour closer to what I desire. Making the image larger so that it would otherwise be clipped by the image view does indeed result in the image view and window growing larger. From this point, however, making the image smaller does not result in the image view or window resizing accordingly. "None" is the only image frame style that displays this somewhat correct behaviour -- all four of the bordered styles (i.e., bevel [which is the default], button, groove, and photo) show the same entirely incorrect behaviour described above.
I came across someone with a similar problem. Oddly, he only observed the problematic behaviour when his image view's frame style was set to NSImageFrameNone, when this is the only value that gives me somewhat-correct behaviour. I tried modifying the frame style to a non-none value before the resize and to none afterward, as this resolved the other person's problem, but for me, this yielded the same behaviour as when I simply set the frame style to "none" initially.
Any help you provide will be much appreciated. Thanks!

Change bounds origin + cropping an image

I am a newbie to Cocoa, I have a few doubts regarding NSImage.
Question1:
Changing the bounds origin of an image doesn't seem to have any effect. I expected the image to be drawn from the newly set origin but that doesn't seem to the case. Am I missing something ?
code:
NSImage* carImage = [NSImage imageNamed:#"car"];
[self.imageView setImage:carImage];
//Following line has no effect:
self.imageView.bounds = CGRectMake(self.imageView.bounds.origin.x + 100, self.imageView.bounds.origin.y, self.imageView.bounds.size.width,self.imageView.bounds.size.height);
Note: imageView is an IBOutlet
Question2:
I was trying to crop an image, but it doesn't seem to be cropping the image, I can see the complete image. What is that I am missing ?
code:
NSRect sourceRect = CGRectMake(150, 25, 100, 50);
NSRect destRect = CGRectMake(0, 0, 100, 50);
NSImage* carImage = [NSImage imageNamed:#"car"];
[carImage drawInRect:destRect fromRect:sourceRect operation:NSCompositeSourceOver fraction:1.0];
[self.imageView setImage:carImage];
Thanks
Changing the bounds origin of an image doesn't seem to have any effect. …
//Following line has no effect:
self.imageView.bounds = CGRectMake(self.imageView.bounds.origin.x + 100, self.imageView.bounds.origin.y, self.imageView.bounds.size.width,self.imageView.bounds.size.height);
That's an image view, not an image.
The effect of changing the bounds of a view depends on what the view does to draw. Effectively, this means you shouldn't change the bounds of a view that isn't an instance of a view class you created, since you can't predict exactly how an NSImageView will draw its image (presumably, since it's a control, it involves its cell, but more than that, I wouldn't rely on).
More generally, it's pretty rare to change a view's bounds origin. I don't remember having ever done it, and I can't think of a reason off the top of my head to do it. Changing its bounds size will scale, not crop.
I was trying to crop an image, but it doesn't seem to be cropping the image, I can see the complete image. What is that I am missing ?
[carImage drawInRect:destRect fromRect:sourceRect operation:NSCompositeSourceOver fraction:1.0];
[self.imageView setImage:carImage];
Telling an image to draw does not change anything about the image. It will not “crop the image” such that the image will thereafter be smaller or larger. You are telling it to draw, nothing more.
Consequently, the statement after that sets the image view's image to the whole image, exactly as if you hadn't told the image to draw, because telling it to draw made no difference.
What telling an image to draw does is exactly that: It tells the image to draw. There are only two correct places to do that:
In between lockFocus and unlockFocus messages to a view or image (or after setting the current NSGraphicsContext).
Within a view's drawRect: method.
Anywhere else, you should not tell any Cocoa object to draw.
One correct way to crop an image is to create a new image of the desired/adjusted size, lock focus on it, draw the desired portion of the original image into it, and unlock focus on the new image. You will then have both the original and a cropped version.
Another correct way would be to create your own custom image view that has two properties: One owning an image to draw, and the other holding a rectangle. When told to draw, this custom view would tell the image to draw the given rectangle into the view's bounds. You would then always hold the original image and simply draw only the desired section.

How can I get NSScrollView to respect a clipping path

I am trying to make a NSScrollView with clipped corners, similar to the Twitter app:
I have a NSScrollView subclass which I added the following code:
- (void)drawRect:(NSRect)dirtyRect {
NSBezierPath *pcath = [NSBezierPath bezierPathWithRoundedRect:[self bounds] xRadius:kDefaultCornerRadius yRadius:kDefaultCornerRadius];
[path setClip];
[super drawRect:dirtyRect];
}
I expected the content of the NSScrollView to have rounded corners, but it is not respecting the clipped path. How can I do this?
UPDATE & CLARIFICATION
I know how to make a custom NSScroller, I know how to make it transparent overlay. All I am asking is how to make the NSSCrollView clip its corners, including everything it contains. The NSScrollView is inside a NSView which has a background that could change, meaning a view overlay to fake the rounded corners is not an option.
After much fiddling, I just discovered that NSScrollView's can be made to have rounded corners by simply giving it a backing layer and setting that layer's corner radius provided you also do the same to it's internal NSClipView. Both are required, which now makes sense, since it's the clip view that actually provides the viewable window into the NSScrollView's document view.
NSScrollView * scrollView = ...;
// Give the NSScrollView a backing layer and set it's corner radius.
[scrollView setWantsLayer:YES];
[scrollView.layer setCornerRadius:10.0f];
// Give the NSScrollView's internal clip view a backing layer and set it's corner radius.
[scrollView.contentView setWantsLayer:YES];
[scrollView.contentView.layer setCornerRadius:10.0f];
You can apply a mask to a view's layer:
[myScrollView setWantsLayer: YES];
[myScrollView layer].mask = ...;
The mask is another CALayer. So in this case you'd create a CALayer, set its background colour to opaque, set its bounds to match the scrollview, and give it a cornerRadius of, say, 8.0.
The result would be that the scroll view and all its contents would appear to be masked to a roundrect with a corner radius of 8px.
Have you tried overriding
- (BOOL)isOpaque {
return NO;
}
And setting the scroll view's -setDrawsBackground: to NO and just leave the view without clipping and just draw the corners with [NSColor clearColor] since this will also clear the underlying color and simulate a round effect.

Resources