How do I reset after a UIScrollView zoom? - iphone

I have a Graph being drawn inside a UIScrollView. It's one large UIView using a custom subclass of CATiledLayer as its layer.
When I zoom in and out of the UIScrollView, I want the graph to resize dynamically like it does when I return the graph from viewForZoomingInScrollView. However, the Graph redraws itself at the new zoom level, and I want to reset the transform scale to 1x1 so that the next time the user zooms, the transform starts from the current view. If I reset the transform to Identity in scrollViewDidEndZooming, it works in the simulator, but throws an EXC_BAD_ACCSES on the device.
This doesn't even solve the issue entirely on the simulator either, because the next time the user zooms, the transform resets itself to whatever zoom level it was at, and so it looks like, if I was zoomed to 2x, for example, it's suddenly at 4x. When I finish the zoom, it ends up at the correct scale, but the actual act of zooming looks bad.
So first: how do I allow the graph to redraw itself at the standard scale of 1x1 after zooming, and how do I have a smooth zoom throughout?
Edit: New findings
The error seems to be "[CALayer retainCount]: message sent to deallocated instance"
I'm never deallocating any layers myself. Before, I wasn't even deleting any views or anything. This error was being thrown on zoom and also on rotate. If I delete the object before rotation and re-add it afterward, it doesn't throw the exception. This is not an option for zooming.

I can't help you with the crashing, other than tell you to check and make sure you aren't unintentionally autoreleasing a view or layer somewhere within your code. I've seen the simulator handle the timing of autoreleases differently than on the device (most often when threads are involved).
The view scaling is an issue with UIScrollView I've run into, though. During a pinch-zooming event, UIScrollView will take the view you specified in the viewForZoomingInScrollView: delegate method and apply a transform to it. This transform provides a smooth scaling of the view without having to redraw it each frame. At the end of the zoom operation, your delegate method scrollViewDidEndZooming:withView:atScale: will be called and give you a chance to do a more high-quality rendering of your view at the new scale factor. Generally, it's suggested that you reset the transform on your view to be CGAffineTransformIdentity and then have your view manually redraw itself at the new size scale.
However, this causes a problem because UIScrollView doesn't appear to monitor the content view transform, so on the next zoom operation it sets the transform of the content view to whatever the overall scale factor is. Since you've manually redrawn your view at the last scale factor, it compounds the scaling, which is what you're seeing.
As a workaround, I use a UIView subclass for my content view with the following methods defined:
- (void)setTransformWithoutScaling:(CGAffineTransform)newTransform;
{
[super setTransform:newTransform];
}
- (void)setTransform:(CGAffineTransform)newValue;
{
[super setTransform:CGAffineTransformScale(newValue, 1.0f / previousScale, 1.0f / previousScale)];
}
where previousScale is a float instance variable of the view. I then implement the zooming delegate method as follows:
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale;
{
[contentView setTransformWithoutScaling:CGAffineTransformIdentity];
// Code to manually redraw view at new scale here
contentView.previousScale = scale;
scrollView.contentSize = contentView.frame.size;
}
By doing this, the transforms sent to the content view are adjusted based on the scale at which the view was last redrawn. When the pinch-zooming is done, the transform is reset to a scale of 1.0 by bypassing the adjustment in the normal setTransform: method. This seems to provide the correct scaling behavior while letting you draw a crisp view at the completion of a zoom.
UPDATE (7/23/2010): iPhone OS 3.2 and above have changed the behavior of scroll views in regards to zooming. Now, a UIScrollView will respect the identity transform you apply to a content view and only provide the relative scale factor in -scrollViewDidEndZooming:withView:atScale:. Therefore, the above code for a UIView subclass is only necessary for devices running iPhone OS versions older than 3.2.

Thanks to all the previous answers, and here is my solution.
Implement UIScrollViewDelegate methods:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return tmv;
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
CGPoint contentOffset = [tmvScrollView contentOffset];
CGSize contentSize = [tmvScrollView contentSize];
CGSize containerSize = [tmv frame].size;
tmvScrollView.maximumZoomScale = tmvScrollView.maximumZoomScale / scale;
tmvScrollView.minimumZoomScale = tmvScrollView.minimumZoomScale / scale;
previousScale *= scale;
[tmvScrollView setZoomScale:1.0f];
[tmvScrollView setContentOffset:contentOffset];
[tmvScrollView setContentSize:contentSize];
[tmv setFrame:CGRectMake(0, 0, containerSize.width, containerSize.height)];
[tmv reloadData];
}
tmv is my subclass of UIView
tmvScrollView — outlet to UIScrollView
set maximumZoomScale and minimumZoomScale before
create previousScale instance variable and set its value to 1.0f
Works for me perfectly.
BR, Eugene.

I was just looking to reset on load to default zoom scale, because when I zoom it, scrollview is having same zoom scale for next time until it gets deallocated.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.scrollView.setZoomScale(1.0, animated: false)
}
OR
-(void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated]
[self.scrollView setZoomScale:1.0f];
}
Changed the game.

I have a detailed discussion of how (and why) UIScrollView zooming works at github.com/andreyvit/ScrollingMadness/.
(The link also contains a description of how to programmatically zoom UIScrollView, how to emulate Photo Library-style paging+zooming+scrolling, an example project and ZoomScrollView class that encapsulates some of the zooming magic.)
Quote:
UIScrollView does not have a notion of a “current zoom level”, because each subview it contains may have its own current zoom level. Note that there is no field in UIScrollView to keep the current zoom level. However we know that someone stores that zoom level, because if you pinch-zoom a subview, then reset its transform to CGAffineTransformIdentity, and then pinch again, you will notice that the previous zoom level of the subview has been restored.
Indeed, if you look at the disassembly, it is UIView that stores its own zoom level (inside UIGestureInfo object pointed to by the _gestureInfo field). It also has a set of nice undocumented methods like zoomScale and setZoomScale:animated:. (Mind you, it also has a bunch of rotation-related methods, maybe we're getting rotation gesture support some day soon.)
However, if we create a new UIView just for zooming and add our real zoomable view as its child, we will always start with zoom level 1.0. My implementation of programmatic zooming is based on this trick.

A fairly reliable approach, appropriate to iOS 3.2 and 4.0 and later, is as follows. You must be prepared to supply your scalable view (the chief subview of the scroll view) in any requested scale. Then in scrollViewDidEndZooming: you will remove the blurred scale-transformed version of this view and replace it with a new view drawn to the new scale.
You will need:
An ivar for maintaining the previous scale; let's call it oldScale
A way of identifying the scalable view, both for viewForZoomingInScrollView: and for scrollViewDidEndZooming:; here, I'm going to apply a tag (999)
A method that creates the scalable view, tags it, and inserts it into the scroll view (here's I'll call it addNewScalableViewAtScale:). Remember, it must do everything according to scale - it sizes the view and its subviews or drawing, and sizes the scroll view's contentSize to match.
Constants for the minimumZoomScale and maximumZoomScale. These are needed because when we replace the scaled view by the detailed larger version, the system is going to think that the current scale is 1, so we must fool it into allowing the user to scale the view back down.
Very well then. When you create the scroll view, you insert the scalable view at scale 1.0. This might be in your viewDidLoad, for example (sv is the scroll view):
[self addNewScalableViewAtScale: 1.0];
sv.minimumZoomScale = MIN;
sv.maximumZoomScale = MAX;
self->oldScale = 1.0;
sv.delegate = self;
Then in your scrollViewDidEndZooming: you do the same thing, but compensating for the change in scale:
UIView* v = [sv viewWithTag:999];
[v removeFromSuperview];
CGFloat newscale = scale * self->oldScale;
self->oldScale = newscale;
[self addNewScalableViewAtScale:newscale];
sv.minimumZoomScale = MIN / newscale;
sv.maximumZoomScale = MAX / newscale;

Swift 4.* and Xcode 9.3
Setting scrollView zoom scale to 0 will reset your scrollView to it's initial state.
self.scrollView.setZoomScale(0.0, animated: true)

Note that the solution provided by Brad has one problem though: if you keep programmatically zooming in (let's say on double tap event) and let user manually (pinch-out) zoom out, after some time the difference between the true scale and the scale UIScrollView is tracking will grow too big, so the previousScale will at some point fall out of float's precision which will eventually result in an unpredictable behaviour

Related

bringSubViewToFront not working as expected

I have an array of views that basically define colours squares on a 5x5 grid. Each view is responsible for its own touch events and, upon touch, performs an animation.
This all works great but sometimes the animation will be clipped by the view's neighbours. I have attempted to fix this with the following code but clipping still sometimes occurs; it seems to happen at random. Is there anything I am missing?
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
[recognizer.view.superview bringSubviewToFront:recognizer.view];
[recognizer.view setNeedsDisplay];
//do animation to the view here
}
Use it as,
[self.View bringSubviewToFront:recognizer.view];
[recognizer.view setNeedsDisplay];

Is it good choice to move a Sublayer around a view using UIPanGestureRecognizer?

I have a CALayer and as sublayer to this CALayer i have added an imageLayer which contains an image of resolution 276x183.
I added a UIPanGestureRecognizer to the main view and calculation the coordinates of the CALayer as follows:
- (void)panned:(UIPanGestureRecognizer *)sender{
subLayer.frame=CGRectMake([sender locationInView:self.view].x-138, [sender locationInView:self.view].y-92, 276, 183);
}
in viedDidLoad i have:
subLayer.backgroundColor=[UIColor whiteColor].CGColor;
subLayer.frame=CGRectMake(22, 33, 276, 183);
imageLayer.contents=(id)[UIImage imageNamed:#"A.jpg"].CGImage;
imageLayer.frame=subLayer.bounds;
imageLayer.masksToBounds=YES;
imageLayer.cornerRadius=15.0;
subLayer.shadowColor=[UIColor blackColor].CGColor;
subLayer.cornerRadius=15.0;
subLayer.shadowOpacity=0.8;
subLayer.shadowOffset=CGSizeMake(0, 3);
[subLayer addSublayer:imageLayer];
[self.view.layer addSublayer:subLayer];
It is giving desired output but a bit slow in the simulator. I have not yet tested it in Device. so my question is - Is it OK to move a CALayer containing an image??
Yes, it is OK to move a CALayer containing an image.
If all you want to do with the panning gesture is move the image, then instead of updating the whole frame, you should just update the layer's position property. Like thus:
- (void)panned:(UIPanGestureRecognizer *)sender {
subLayer.position=CGPointMake([sender locationInView:self.view].x, [sender locationInView:self.view].y);
}
Two things:
First, you can't draw ANY conclusions based on the performance of the simulator. Some things on the simulator are an order of magnitude faster than on a device, and other things are significantly slower. Animation is especially a mixed bag.
If you're doing performance-critical work, test it on the device, early and often.
Second, you can certainly animate a layer using a gesture recognizer, but that is an awfully round-about way to do it. Gesture recognizers are designed to work on views, and it's much easier and cleaner to tie the recognizer to a subview rather than a sub layer.
One of the big problems you will have with using a layer is hit-testing. If you let go of your image, then try to drag it some more, you'll have to have the gesture on the containing view, take the gesture coordinates and do hit testing on the layer. Ugh.
Take a look at the gesture based version of the touches sample app from Apple. It shows you how to cleanly move UIView objects around the screen using gestures.
Note that you can create a view that has custom layer content, and drag that around.

Changing frame of UIView's CALayer (self.view.layer.frame = ...) appears to have no effect

I'm sure I'm missing something basic here. I'm trying out the CALayers 'hello world' code from:
http://www.raywenderlich.com/2502/introduction-to-calayers-tutorial
Doing the very first example. New single view project in xcode 4.2. No change to the nib/storyboard. Import QuartzCore. Add the following code to ViewDidLoad in the ViewController.m:
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.layer.backgroundColor = [UIColor blueColor].CGColor;
self.view.layer.cornerRadius = 30.0;
self.view.layer.frame = CGRectMake(20, 20, 20, 20);
}
I run this (ipad 2 or ipad simulator) and get a full screen blue rectangle with rounded corners. What I hoped to get was a 20x20 blue rectangle offset by 20/20.
I'm clearly getting control over the views layer (as shown by the color and rounded corners). However, adjusting the frame seems to have no impact. I've NSLog'ed the frame before/after setting it, and it has changed. Is the frame of the root layer locked to the uiview frame?
I don't have a strong reason to change the views layers frame, I'm just trying to reason through what is going on. Hopefully this is an easy question...
Thanks!
Paul
Actually, the previous answer (you can't set uiview.layer.frame as it always fills the uiview) is close, but not quite complete. After reading the answer, I registered for the original site and to comment that the tutorial had issues. In doing so, I found that there were already comments that I hadn't seen in my first pass that addressed this. Using those, I started doing some testing.
The bottom line, if you move the self.view.layer.frame setting code from viewDidLoad to viewWillAppear, it works fine. That means that you can change the frame of the root layer of a view. However, if you do it in viewDidLoad it will be undone later.
However, the previous answer is still pretty close. I NSLog'ed the frame of the root layer and the frame of the view. Changing the root layer frame changes the view frame. So, the answer that the view.layer.frame always fills the view.frame is correct. However, setting the layer frame resets the view frame to match. (I'm guessing that uiview.frame property simply returns uiview.layer.frame...)
So, at some point in time between 2010 and today, something in the environment changed. Specifically, after viewDidLoad and before viewWillAppear the uiview/layer frame appears to be reset to the nib specified value. This overrides any changes in viewDidLoad. Changes made in viewWillAppear appear to stick.
Robin's answer got me on the right track, but I wanted to spell out the full answer.
The tutorial is wrong. Setting the frame of the view's main layer has no effect. The main layer is 'special' and will always fill the view's bounds. What you need to do is create a sublayer of the main layer like this:
- (void)viewDidLoad
{
[super viewDidLoad];
CALayer *newLayer = [[CALayer alloc] init];
newLayer.backgroundColor = [UIColor orangeColor].CGColor;
newLayer.cornerRadius = 20.0;
newLayer.frame = CGRectMake(100.0f, 100.0f, 200.0f, 200.0f);
[self.view.layer addSublayer:newLayer];
[newLayer release]; // Assuming you're not using ARC
}
Also, in your code a layer with width 20pt and height 20pt is too small to have rounded corners of 30pt anyway.

How to add a gradient to a scrollview?

I am trying to add a bunch of gradients to different UIScrollView's and UITableView's. The most common example of how to achieve this that i have come across is from Matt Galagher's awesome blog where he posted an example of how to use gradients here:
http://cocoawithlove.com/2009/08/adding-shadow-effects-to-uitableview.html
My question though is what advantages/benefits do you gain by inserting the gradient in the layoutSubviews method vs setting up the gradient in the viewDidLoad method? I realize that by goin the viewDidLoad route you would have to update the view manually when the orientation changes but it would seem from a performance standpoint that this method would only be called once when the view loads and then again when the orientation changes. In contrast the layoutSubviews method gets called everytime time the view changes which in the case of a scrollview/tableview is a lot!
//
// Construct the origin shadow if needed
//
if (!originShadow)
{
originShadow = [self shadowAsInverse:NO];
[self.layer insertSublayer:originShadow atIndex:0];
}
else if (![[self.layer.sublayers objectAtIndex:0] isEqual:originShadow])
{
[self.layer insertSublayer:originShadow atIndex:0];
}
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
//
// Stretch and place the origin shadow
//
CGRect originShadowFrame = originShadow.frame;
originShadowFrame.size.width = self.frame.size.width;
originShadowFrame.origin.y = self.contentOffset.y;
originShadow.frame = originShadowFrame;
[CATransaction commit];
He also seems to be resizing the frame everytime the method is called? Wouldnt it be better to initialize and size the gradient in the viewDidLoad method and then do any resizing when/if the orientation changes?
Pretty sure im missing something here so any clarification would be appreciated.
Thx
I wouldn't create the gradient in layoutSubviews. Layout subviews might get called more often than is optimal for this kind of drawing. By doing something CPU intensive in layoutSubviews, like creating a gradient, you might adversely impact performance.
Instead, what I would do is put your gradient on a subview and then add your subview into your scrollview (viewDidLoad, init, or awakeFromNib is good for this) and let it handle it's drawing automatically. Then, in layoutSubviews, adjust the layout of your subview, and let it handle figuring out what portions of the view need to be redrawn to update things.

Lazy load pages in UIScrollView

I have a UIScrollView that contains large images and am using paging to scroll between images. In order to save memory, I am loading only one image before and after the currently visible one and loading/releasing new images after a scroll has completed.
The problem occurs when one scrolls quickly and scrollViewDidEndDecelerating is not called.
How can I detect continuous scrolling?
I could check item location on every scrollViewDidScroll but this seems a bit heavy...
Perhaps a table view with custom cell content would work better for you, since it has a lot of logic built in for only loading cells that are visible as opposed to everything at once. There are lots of tutorials for how to manage table views in various ways.
My temporary solution is to cancel continuous scrolling by disabling scroll from when the user lifts the finger until scroll has completed.
-(void) scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
[scrollView setScrollEnabled:NO];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[scrollView setScrollEnabled:YES];
}
if you are still looking for a solution... this what I do, it works really good having a good performance, too.
- (void)scrollViewDidEndDragging:(UIScrollView *)sv willDecelerate:(BOOL)decelerate
{
if (!decelerate)
{
// load images and unload the ones that are not needed anymore
[self someMethod]
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sv
{
// load images and unload the ones that are not needed anymore
[self someMethod]
}
You have to check both scrollViewDidEndDecelerating and scrollViewDidScroll.
When to user swipes and releases it letting to have the "momentum" scroll the first will be called at the and. If the user decides to stop the scroll with his finger by tapping to the tableview (or scroll comes to the most bottom, but I am not sure about that) the second event is fired.
You can do something like this
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth / 1.5) / pageWidth) + 1;
}
this method is called anytime the contentoffset changed whether programmatically or not.
From this method you can check to see if 'page' is the same as your current page, if not, you know you need to load something. The trick here is making images load without holding up the scrolling of the scrollview. That is where I am stuck.
the page calculation will change to next/previous when you are half way to the next page
As per my experience, #jd291 is correct; I am using following callbacks successfully for most places
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
but, the only case when a callback is not called is that you may not had set up the delegate properly.
The WWDC 2010 has talked about this. They do a reuse of the sub scrollviews, alloc the missing image view in scrollViewDidScroll:. The video may help:
Session 104 - Designing Apps with Scroll Views