20

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.

Kampai
  • 22,848
  • 21
  • 95
  • 95
Ed Marty
  • 39,590
  • 19
  • 103
  • 156

7 Answers7

44

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.

Jagat Dave
  • 1,643
  • 3
  • 23
  • 30
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • why the content view keep disappearing? – domlao Dec 06 '09 at 16:56
  • 1
    You haven't provided much information, but if you are not manually hiding or removing the content view from the hierarchy, my guess is that it is becoming larger than the texture size supported by the GPU. If the view becomes wider or taller than 2048 pixels, it can no longer be rendered on the iPhone and iPhone 3G (I can't speak for the 3G S). – Brad Larson Dec 06 '09 at 20:14
  • I redraw it successfully and it was shard and clear. But I notice it slows down the zooming. Do you have any idea why? Thanks. – domlao Dec 08 '09 at 18:01
  • Which part of the zooming? The pinch-zooming in and out should be pretty smooth, because the view is simply being transformed during that part of the zoom operation. If the application is slow in redrawing your view at the end of the zoom, you might need to take a look at your draw operations using Shark or Instruments to see where your bottlenecks are. – Brad Larson Dec 08 '09 at 22:12
  • I have a grid of UIImageView in a UIView. Probably its my phone, I'm using 1st generation phone. – domlao Dec 08 '09 at 23:22
  • @Brad Larson, but even in > 3.2 you should "transform on your view to be CGAffineTransformIdentity and then have your view manually redraw itself at the new size scale," right? Otherwise your view comes out fuzzy (for text). – Dan Rosenstark Jul 23 '10 at 20:57
  • @Yar - Yes, the identity transform still needs to be applied and the content view re-rendered for sharp display. The earlier problem was that UIScrollView ignored when you reset the transform on the content view and kept trying to transform it based on an absolute scale factor, thus my workaround for that. This has been corrected in OS 3.2+. – Brad Larson Jul 23 '10 at 21:20
  • Thanks so much @Brad Larson. I've put another question at http://stackoverflow.com/questions/3322547 which is what I'm struggling with. I've gotten pretty far, which probably means that I'm on the totally wrong track :) – Dan Rosenstark Jul 23 '10 at 21:32
  • Setting the content view's transform to CGAffineTransformIdentity also sets the scroll view's zoomScale to 1. This means that if bouncesZoom is NO the view can no longer be zoomed back down to its original size, because the scroll view now thinks it *is* at is original size. This hack ignores such complications and so won't work in the general case. – matt Nov 11 '10 at 18:41
  • @BradLarson can u please help me out on this ? http://stackoverflow.com/q/19856928/935381 – Hitarth Nov 11 '13 at 06:39
14

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.

dymv
  • 3,252
  • 2
  • 19
  • 29
12

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.

Preetam Jadakar
  • 4,479
  • 2
  • 28
  • 58
5

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.

Andrey Tarantsov
  • 8,965
  • 7
  • 54
  • 58
  • zoomScale is part of UIScrollView, not UIView. – Jonny Oct 29 '10 at 08:40
  • 1
    I think you've moved your Scrolling Madness content now? Would you be able to update the link (I've not managed to find the new home). – Benjohn Sep 21 '15 at 13:24
  • 1
    @Benjohn Hey, it got hopelessly outdated a good number of years ago. But now you can just reset zoomScale on UIScrollView, so this trickery is no longer needed (since iOS 3 or 4). – Andrey Tarantsov Sep 21 '15 at 19:54
3

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;
matt
  • 515,959
  • 87
  • 875
  • 1,141
3

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)
Prashant Gaikwad
  • 3,493
  • 1
  • 24
  • 26
  • Do you mean 1.0 rather? self.scrollViewForZoom.setZoomScale(1.0, animated: true) – iOS Flow Oct 19 '18 at 08:45
  • No. If you want zoom use self.scrollView.zoom(to: rectTozoom, animated: true) and If you want to zoom out then use self.scrollView.setZoomScale(0.0, animated: true). – Prashant Gaikwad Oct 24 '18 at 05:57
2

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

esad
  • 2,660
  • 1
  • 27
  • 23