I derived a class from UIView, only to realize that there are real limitations on its size due to memory. I have this UIView inside UIScrollView.
Is there a way for me to put something inside a scroll view that is not a UIView-derived class but into which I can still draw, and which can be very very large?
I don't mind having to respond to expose-rectangle events, like one does when using conventional windowing systems.
Thanks.
The things inside of a UIScrollView must be UIViews, which are size-restricted for memory reasons. UIView maintains a bitmapped backing store for performance reasons, so it has to allocate memory proportional to its size.
The usual way that you handle this is to generate several UIViews and swap them out as the user scrolls around. The other version of that is to use CATiledLayer. Neither of those give you the "giant canvas" drawing model, though. It's up to you to break things up and draw them as needed. This is the usual approach, though.
If you really want a giant canvas, my recommendation would be a CGPDFContext. There is rich existing support for these, particularly using UIWebView (remember, you can open data: URIs to avoid reading files from disk). And you can draw parts of them directly by applying affine transforms and then CGContextDrawPDFPage. CGBitmapContext is another approach, but it could require a lot more memory for a small amount of drawing.
So you have a UIView inside a UIScrollView, but you want your UIView to have very large bounds (i.e., so it matches the size of your UIScrollView's contentSize). But you don't want to draw the entire UIView every time it needs displaying, nor can you fit its entire contents in memory at once.
Make your UIView uses a CAScrollLayer backing, as follows:
// MyCustomUIView.m
+ (Class) layerClass
{
return [CAScrollLayer class];
}
Add a method to update the scroll position when the user scrolls the UIScrollView containing your UIView:
// MyCustomUIView.m
- (void) setScrollOffset:(CGPoint)scrollOffset
{
CAScrollLayer *scrollLayer = (CAScrollLayer*)self.layer;
[scrollLayer scrollToPoint:scrollOffset];
}
Ensure that when you draw your UIView, you only draw the portions contained in the CGRect provided to you:
- (void)drawRect:(CGRect)rect
{
// Only draw stuff that lies inside 'rect'
// CGRectIntersection might be handy here!
}
Now, in your UIScrollViewDelegate, you'll need to notify your CAScrollLayer backed view when the parent UIScrollView updates:
// SomeUIScrollViewDelegate.m
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
// Offset myCustomView within the scrollview so that it is always visible
myCustomView.frame = CGRectMake(scrollView.contentOffset.x,
scrollView.contentOffset.y,
scrollView.bounds.size.width,
scrollView.bounds.size.height);
// "Scroll" myCustomView so that the correct portion is rendered
[myCustomView setScrollOffset:self.contentOffset];
// Tell it to update its display
[myCustomView setNeedsDisplay];
}
You can also use CATiledLayer, which is easier because you do not have to track the scroll position — instead your drawRect method will be called with each tile as-needed. However this will cause your view to fade in slowly. It might be desirable if you intend to cache parts of your view and don't mind the slow updates.
Related
I'm well aware that there's no nice way to render a small part of a UIView to an image, besides rendering the whole view and cropping. (VERY expensive on something like an iPad 3, where the resulting image is huge). See here for a description of the renderInContext method (there's no alternatives).
The part of the view that I want to render can't be predetermined, meaning I can't set up the view hierarchy for the small section to be it's own UIView (and therefore CALayer).
My Idea...
I had an idea, but I need some direction if I'm going to succeed. What if I create a category on UIView (or CALayer) that adds a method along the lines of:
[UIView renderSubframe:(CGFrame)frame];
How? Well, I'm thinking that if a dummy view the size of the sub frame was created, then the view could be shifted temporarily onto this. Then the dummy view's layer could call renderInContext:, resulting in an efficient and fast way of getting views.
So...
I'm really not that up to speed with CALayer/Quartz core stuff... Will this have any chance of working? If not, what's another way I could achieve the same thing - what's the most efficient way I could achieve the same result (or has anyone else already faced this problem and come up with a different solution)?
Here's the category I ended up writing. Not as hard as I first thought, thanks to a handy CGAffineTransform.
#import "UIView+RenderSubframe.h"
#import <QuartzCore/QuartzCore.h>
#implementation UIView (RenderSubframe)
- (UIImage *) renderWithBounds:(CGRect)frame {
CGSize imageSize = frame.size;
UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextConcatCTM(c, CGAffineTransformMakeTranslation(-frame.origin.x, -frame.origin.y));
[self.layer renderInContext:c];
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return screenshot;
}
#end
A couple of different ways you might do this:
specify a CGContextClipToRect for your context before calling renderInContext;
use:
setNeedsDisplayInRect:
Marks the region within the specified rectangle as needing to be updated.
- (void)setNeedsDisplayInRect:(CGRect)theRect
This would make the rendering happen only in the specified rect. I am not sure though whether this would work as per your requirement.
I think that the first option should work seamlessly.
In my app I have a scrollview which has some objects like an imageview, a button etc. and my scrollview has a huge content so I need to scroll vertically or horizontally to explore it. My question is how can I check if an object is located at a visible position on the phone's screen?
Edit : I want to detect if the image is completely in the scrollView's frame or not. however CGRectIntersectsRect(bottomScroller.bounds, selectImage_1.frame) returns true even if there is a small intersection.
Compare your object's (imageview, a button etc.) frame with the scrollView.bounds:
if (CGRectIntersectsRect(scrollView.bounds, object.frame)) ...
EDIT:
If you want to ensure that the object is fully displayed, then calculate the intersection and compare it to the object's frame:
CGRectEqualToRect(
CGRectIntersection(scrollView.bounds, object.frame),
object.frame);
although you might found an optimized way to do that check.
As stated in the other answers, you'll want to use CGRectIntersectsRect as this will output only when your object is in intersection with the visible frame of the scroll view. However, in order to make this more complete, you should use UIScrollViewDelegate's method scrollViewDidScroll which will allow you to more or less ask for the intersection when ever scrolling occurs. Here's an example:
.h
<UIScrollViewDelegate>
.m
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (CGRectIntersectsRect(mySubView.frame, scrollView.bounds)) {
CGRect intersection = CGRectIntersection(mySubView.frame, scrollView.bounds);
NSLog(#"%#",NSStringFromCGRect(intersection));
}
}
EDIT: Then using CGRectIntersectsRect you can ask for the specific intersection and at this point all you have to do to make this depended on your needs is write something like if(intersect.x > y)//do something.
I need to draw incrementally in a subclassed UIView, but the view is cleared every time I call [self setNeedsDisplay].
I'm doing this:
- (id)initWithCoder:(NSCoder *)aDecoder {
if ((self = [super initWithCoder:aDecoder])) {
self.clearsContextBeforeDrawing = FALSE;
}
return self;
}
Am I missing something? How do I stop UIView from clearing?
If you call setNeedsDisplay it will call drawRect with the full bounds.
If you are drawing incrementally (portions of the view), you need to call:
[self setNeedsDisplayInRect:rect];
Of course that means your drawRect needs to be able to handle just drawing a portion of the view.
More info here: Drawing incrementally in a UIView (iPhone)
Also, you're setting it to FALSE and it's typically NO in objective-c. One resolves to 0 and the other to (BOOL)0 at compile time. A bit odd but not causing issues:
Is there a difference between YES/NO,TRUE/FALSE and true/false in objective-c?
Sorry.
drawRect: is not guaranteed use the saved contents of any previous drawing of a regular UIView. The framebuffer is likely behind an opaque path to the GPU, and can be thought of as essentially write only. So you always have to be able to recreate the entire view if you implement a drawRect for a regular UIView.
But you can draw into another context including previous graphics contents if you create your own bitmap drawing context, and draw into that context instead of a UIView. You can then convert that bitmap context into an image and display that image as the content of the CALayer of a UIView.
I want to draw a simple line on the iPhone by touching and dragging across the screen. I managed to do that by subclassing UIView and change the default drawRect: method. At the same time in my viewcontroller I detect the touch event and call the [myView setNeedsDisplay] when necessary. The problem is that when I try to draw the second line the previous line disappears. Is there a way to keep the previous line on the screen?
Any input will be very much appreciated.
The usual method is to use CGBitmapContextCreate(). Create it in -init/-init-WithFrame:/etc and call CGContextRelease() in -dealloc. I'm not sure how you handle the 2x scale of the "retina display" with CGBitmapContextCreate().
(UIGraphicsBeginImageContext() is easier, but it might not be safe to do UIGraphicsBeginImageContext(); myContext = CFRetain(UIGraphicsGetCurrentContext()); UIGraphicsEndImageContext();.)
Then do something like
#import <QuartzCore/CALayer.h>
-(void)displayLayer:(CALayer*)layer
{
UIGraphicsPushContext(mycontext);
... Update the bitmap context by drawing a line...
UIGraphicsPopContext();
CGImageRef cgImage = CGBitmapContextCreateImage(mycontext);
layer.contents = (id)cgImage;
CFRelease(cgImage);
}
I've used -displayLayer: (a CALayer delegate function; a UIView is its layer's delegate) instead of -drawRect: for efficiency: if you use -drawRect:, CALayer creates a second context (and thus a second copy of the bitmap).
Alternatively, you might have luck with CGLayer. I've never seen a reason to use it instead of a bitmap context; it might be more efficient in some cases.
Alternatively, you might get away with setting self.clearsContextBeforeDrawing = NO, but this is very likely to break. UIKit (or, more accurately, CoreAnimation) expects you to draw the whole view contained in the clip rect (that's the "rect" argument of -drawRect:; it's not guaranteed to be the bounds). If your view goes offscreen, CoreAnimation might decide that it wants the memory back. Or CoreAnimation might only draw the part of the view that's on-screen. Or CoreAnimation might do double-buffered drawing, causing your view to appear to flip between two states.
If you use drawRect: to draw, then you need to draw the whole area. So you need to store not only the data for the latest part but everything.
As an alternative, you might draw directly into a bitmap, or generate dynamically subviews for your lines (makes only sense for very limited drawing (i.e. some few vector-based stuff).
I have an instance of UIScrollview containing an instance of UIView. The UIView is just a container for a horizonal array of UIImageView instances. - Zooming is provided by UIScrollView and UIScrollViewDelegate. I would like to constrain zooming to occur only along the horizontal axis with no vertical scalling at all. How to I do this?
Is there a way, for example, to subclass UIView and override the appropriate method to prevent vertical scaling? I like this approach but I am unclear on which method to override and what that overridden method should actually do.
Cheers,
Doug
Similar to what I describe in this answer, you can create a UIView subclass and override the -setTransform: accessor method to adjust the transform that the UIScrollView will try to apply to your UIView. Set this UIView to host your content subviews and make it the subview of the UIScrollView.
Within your overridden -setTransform:, you'll need to take in the transform that the UIScrollView would like to apply and adjust it so that the scaling only takes effect in one direction. From the documentation on how CGAffineTransform matrices are constructed, I believe the following implementation should constrain your scaling to be just along the horizontal direction:
- (void)setTransform:(CGAffineTransform)newValue;
{
CGAffineTransform constrainedTransform = CGAffineTransformIdentity;
constrainedTransform.a = newValue.a;
[super setTransform:constrainedTransform];
}
Using OS 3.0, you can tell the zoom to zoom to a rect in the scrollview. I have this in my logic that detects taps.
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:CGPointMake(tapPoint.x, tapPoint.y) inScrollView:scrollView];
[scrollView zoomToRect:zoomRect animated:YES];
The for the other part, you will have to stretch your image views by the ratio that the new frame has against the original, and center it in the same center point. You can do this in an animation timed the same as the zoom animation so that it looks right, but I think this will be the only way to do it.
In scrollViewDidZoom:, adjust your content view's variables based on zoomScale, reset zoomScale to 1.0, then do setNeedsDisplay on the content view. Handle the actual zoom (in whatever direction you want) in your content view's drawRect:.
The Ugly Details:
While the zoom is in progress, the UIScollView changes contentOffset and contentScale, so save those prior values in scrollViewWillBeginZooming: and in scrollViewDidZoom: so you can compute a new contentOffset yourself according to the zoom.
Since changing zoomScale will immediately fire another scrollViewDidZoom:, you must set a BOOL before (and clear after) resetting the zoomScale. Test the BOOL at the start of scrollViewDidZoom: and return if true.
You may need to inhibit scrollViewDidScroll: while the zoom is in progress (test a BOOL; set it in scrollViewWillBeginZooming: and clear it in scrollViewDidEndZooming:) so your own contentOffsets are used while the zoom is in progress.