76

How can I go about finding out the rect (CGRect) of the content of a displayed view that is actually visible on screen.

myScrollView.bounds

The code above works when there's no zooming, but as soon as you allow zooming, it breaks at zoom scales other than 1.

To clarify, I want a CGRect that contains the visible area of the scroll view's content, relative to the content. (ie. if it's a zoom scale 2, the rect's size will be half of the scroll view's size, if it's at zoom scale 0.5, it'll be double.)

Ajumal
  • 1,048
  • 11
  • 33
Kenneth Ballenegger
  • 2,970
  • 2
  • 19
  • 25

8 Answers8

115

Or you could simply do

CGRect visibleRect = [scrollView convertRect:scrollView.bounds toView:zoomedSubview];

Swift

let visibleRect = scrollView.convert(scrollView.bounds, to: zoomedSubview)
Trenskow
  • 3,783
  • 1
  • 29
  • 35
  • 2
    zoomedSubview is the subview you wan't to get the visible rect for. – Trenskow Mar 30 '13 at 10:58
  • 3
    zoomedSubview is the UIView that's being zoomed in/out or scrolled in the "scrollView". It's the same UIView that you return from your implementation of UIScrollViewDelegate's (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView method. – Alex the Ukrainian May 14 '13 at 20:30
85

Answering my own question, mostly thanks to Jim Dovey's answer, which didn't quite do the trick, but gave me the base for my answer:

CGRect visibleRect;
visibleRect.origin = scrollView.contentOffset;
visibleRect.size = scrollView.bounds.size;

float theScale = 1.0 / scale;
visibleRect.origin.x *= theScale;
visibleRect.origin.y *= theScale;
visibleRect.size.width *= theScale;
visibleRect.size.height *= theScale;

The main difference is that the size of the visibleRect ought to be scrollView.bounds.size, rather than scrollView.contentSize which is the size of the content view. Also simplified the math a bit, and didn't quite see the use for the isless() which would break the code whenever it's greater.

Kenneth Ballenegger
  • 2,970
  • 2
  • 19
  • 25
  • This appears to fail with iOS 3.2+, and CATiledLayer's - the scrollView's zoomScale is always set to (e.g. on iPad: ) "2.0" no matter what. I know UIScrollView was changed in iOS 3.2 - perhaps it broke this workaround? Or is it CATiledLayer that breaks it? Sigh – Adam Dec 29 '11 at 21:17
  • UPDATE: this *does* work with iOS 4+ - unless you forget to implement the UIScrollViewDelegate method "scrollViewDidEndZooming:withView:atScale:". NB: the method can be empty! But it's an Apple implementation bug that if the method is not there ... Apple FAILS to update their own zoomScale property (and who knows what other bugs?) – Adam Dec 29 '11 at 23:57
  • @kenneth Ballenegger what is the "scale" (float theScale = 1.0 / scale;) in your code ? – Hitarth Jan 24 '13 at 10:42
  • 3
    Actually, Trenskow's answer below is much better. Let the system do it for you. – n13 Mar 06 '13 at 16:51
29

You have to compute it using UIScrollView's contentOffset and contentSize properties, like so:

CGRect visibleRect;
visibleRect.origin = scrollView.contentOffset;
visibleRect.size = scrollView.contentSize;

You can then log it for sanity-testing:

NSLog( @"Visible rect: %@", NSStringFromCGRect(visibleRect) );

To account for zooming (if this isn't already done by the contentSize property) you would need to divide each coordinate by the zoomScale, or for better performance you would multiply by 1.0 / zoomScale:

CGFloat scale = (CGFloat) 1.0 / scrollView.zoomScale;
if ( isless(scale, 1.0) )      // you need to #include <math.h> for isless()
{
    visibleRect.origin.x *= scale;
    visibleRect.origin.y *= scale;
    visibleRect.size.width *= scale;
    visibleRect.size.height *= scale;
}

Aside: I use isless(), isgreater(), isequal() etc. from math.h because these will (presumably) do the right thing regarding 'unordered' floating-point comparison results and other weird & wonderful architecture-specific FP cases.


Edit: You need to use bounds.size instead of contentSize when calculating visibleRect.size.

Lord Zsolt
  • 6,492
  • 9
  • 46
  • 76
Jim Dovey
  • 11,166
  • 2
  • 33
  • 40
10

Shorter version:

CGRect visibleRect = CGRectApplyAffineTransform(scrollView.bounds, CGAffineTransformMakeScale(1.0 / scrollView.zoomScale, 1.0 / scrollView.zoomScale));

I'm not sure if this is defined behavior, but almost all UIView subclasses have the origin of their bounds set to (0,0). UIScrollViews, however, have the origin set to contentOffset.

Frank Schmitt
  • 25,648
  • 10
  • 58
  • 70
6

a little more general solution would be:

 [scrollView convertRect:scrollView.bounds
                             toView:[scrollView.delegate viewForZoomingInScrollView:scrollView]];
Nikolay Shubenkov
  • 3,133
  • 1
  • 29
  • 31
3
CGRect visibleRect;
visibleRect.origin = scrollView.contentOffset;
visibleRect.size = scrollView.frame.size;
Bas Dirkse
  • 31
  • 1
1

Swift 4.0:

My answer adapts Trenskow's answer to Swift 4.0:

let visible = scrollView.convert(scrollView.bounds, to: subView)

where scrollView is the view of the scroll, and subView is the view inside scrollView which is zoomable and contains all the contents inside the scroll.

RoberRM
  • 883
  • 9
  • 18
0

I don't think that a UIScrollView gives you that rectangle directly, but I think you have all the necessary items to calculate it.

A combination of the bounds, the contentOffset and the zoomScale should be all you need to create the rectangle you are looking for.

Jonathan Arbogast
  • 9,620
  • 4
  • 35
  • 47