UIScrollView disable scrolling while rotating on iPhone/iPad - iphone

I am using UIScrollView and an image in it as paging one image per page. I have a problem while rotating the iPhone
When I rotate the iPhone then scrollViewDidScroll (Scroll view delegate method) is calling.
Due to this, my paging is disturbed and the page number changes.
What is the solution?

Raphaël's answer is an excellent description of the problem, and a neat fix. I had the exact same problem and ended up fixing with a scrollingLocked flag that I set to YES (locked) before the rotation starts, and NO (unlocked) when it ends. Perhaps slightly less hacky than temporarily changing the contentSize:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation
duration:(NSTimeInterval)duration
{
self.photoViewer.scrollingLocked = YES;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromOrientation
{
self.photoViewer.scrollingLocked = NO;
}
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
if (self.scrollingLocked)
{
return;
}
/* do normal scrollViewDidScroll: stuff */
}

I found a strange undocumented behavior when rotating a paged UIScrollView.
When the scrollview is positioned at the last page and the user changes the orientation, the OS scrolls the UIScrollView a few pixels back to compensate for the difference between height and width.
Basically I received the following calls for any page.
willRotateToInterfaceOrientation:duration
willAnimateRotationToInterfaceOrientation:duration:
didRotateFromInterfaceOrientation:
And for the last page:
willRotateToInterfaceOrientation:duration
scrollViewDidScroll:
willAnimateRotationToInterfaceOrientation:duration:
didRotateFromInterfaceOrientation:
That messed up with my pages too. The problem is that in willRotate, the bounds have not been updated by the OS yet, and in willAnimate you have the new bounds and can compute the new size, but it's too late...
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
CGSize tempSize = [self.pagingScrollView contentSize];
NSUInteger padding = abs(pagingScrollView.frame.size.width - pagingScrollView.frame.size.height);
tempSize.width += padding;
[self.pagingScrollView setContentSize:tempSize];
[...]
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration{
CGSize newSize = ... // Compute new content size based on new orientation
[self.pagingScrollView setContentSize:newSize];
}
This is just a workaround, but I spent countless hours on this issue and could not find an elegant solution.

Swift 4 solution:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
lui.l("apply previousTraitCollection: \(previousTraitCollection)")
canScroll = true
}

You can try this method for Swift:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
// Execute before rotation
}) { _ in
//Execute after rotation
}
}

My task is to allow scrolling the landscape. The design is for portait. I came up with an idea to add a ScrollView to components, or in "Embed in Scroll View" in Interface Builder. I have expected it will work, but no. I am using Xcode 4.4, iOS 5.1, (office project need support for 4.2 too), but the problem is the same.
In Stack Overflow question iPhone SDK: UIScrollView does not scroll there is one row which solve a problem.
Other try is in Stack Overflow question iOS - UIScrollView is not working (it doesn't scroll at all - the image stays fixed), and this helped me, combined with other, so here is my portait-to-scrollable landscape code:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromOrientation
{
if( UIInterfaceOrientationIsPortrait( [[UIApplication sharedApplication] statusBarOrientation] ) ){
scrollView.contentSize = portaitScrollSize;
}
else{//statusbar is Landscape
scrollView.contentSize = landscapeScrollSize;
}
}
The scrollView in bound to an iVar view in Interface Builder. portaitScrollSize and landscapeScrollSize are private variables. They are initialized and doesn't change.
In my.h file:
IBOutlet UIScrollView *scrollView;
In my.m file:
CGSize portaitScrollSize, landscapeScrollSize;
...
portaitScrollSize = CGSizeMake(320,440);
landscapeScrollSize = CGSizeMake(480,480);
I hope it will help somebody to add a rotating + scroll feature to a portait design.
Don't forget to allow portait+landscape on the top component:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return TRUE;
}

In addition to Raphaël Mor's answer. If you are switching from portrait to landscape, the contentsize and the page structure will brake. Therefore, in order to maintain the current page structure just add extra content size to width:
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{
[self.scrollView setContentSize:CGSizeMake(self.scrollView.contentSize.width + 400, self.scrollView.contentSize.height)];
}
And make sure you set the contentsize and offset again after the orientation changed:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self.scrollView setContentSize:CGSizeMake(self.scrollView.bounds.size.width *3, self.scrollView.bounds.size.height)];
[self.scrollView setContentOffset:CGPointMake(self.scrollView.bounds.size.width * self.pageControl.currentPage, 0) animated:NO];
}

Related

Dynamically setting layout on UICollectionView causes inexplicable contentOffset change

According to Apple's documentation (and touted at WWDC 2012), it is possible to set the layout on UICollectionView dynamically and even animate the changes:
You normally specify a layout object when creating a collection view but you can also change the layout of a collection view dynamically. The layout object is stored in the collectionViewLayout property. Setting this property directly updates the layout immediately, without animating the changes. If you want to animate the changes, you must call the setCollectionViewLayout:animated: method instead.
However, in practice, I've found that UICollectionView makes inexplicable and even invalid changes to the contentOffset, causing cells to move incorrectly, making the feature virtually unusable. To illustrate the problem, I put together the following sample code that can be attached to a default collection view controller dropped into a storyboard:
#import <UIKit/UIKit.h>
#interface MyCollectionViewController : UICollectionViewController
#end
#implementation MyCollectionViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:#"CELL"];
self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"CELL" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
[self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
NSLog(#"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
}
#end
The controller sets a default UICollectionViewFlowLayout in viewDidLoad and displays a single cell on-screen. When the cells is selected, the controller creates another default UICollectionViewFlowLayout and sets it on the collection view with the animated:YES flag. The expected behavior is that the cell does not move. The actual behavior, however, is that the cell scroll off-screen, at which point it is not even possible to scroll the cell back on-screen.
Looking at the console log reveals that the contentOffset has inexplicably changed (in my project, from (0, 0) to (0, 205)). I posted a solution for the solution for the non-animated case (i.e. animated:NO), but since I need animation, I'm very interested to know if anyone has a solution or workaround for the animated case.
As a side-note, I've tested custom layouts and get the same behavior.
UICollectionViewLayout contains the overridable method targetContentOffsetForProposedContentOffset: which allows you to provide the proper content offset during a change of layout, and this will animate correctly. This is available in iOS 7.0 and above
I have been pulling my hair out over this for days and have found a solution for my situation that may help.
In my case I have a collapsing photo layout like in the photos app on the ipad. It shows albums with the photos on top of each other and when you tap an album it expands the photos. So what I have is two separate UICollectionViewLayouts and am toggling between them with [self.collectionView setCollectionViewLayout:myLayout animated:YES] I was having your exact problem with the cells jumping before animation and realized it was the contentOffset. I tried everything with the contentOffset but it still jumped during animation. tyler's solution above worked but it was still messing with the animation.
Then I noticed that it happens only when there were a few albums on the screen, not enough to fill the screen. My layout overrides -(CGSize)collectionViewContentSize as recommended. When there are only a few albums the collection view content size is less than the views content size. That's causing the jump when I toggle between the collection layouts.
So I set a property on my layouts called minHeight and set it to the collection views parent's height. Then I check the height before I return in -(CGSize)collectionViewContentSize I ensure the height is >= the minimum height.
Not a true solution but it's working fine now. I would try setting the contentSize of your collection view to be at least the length of it's containing view.
edit:
Manicaesar added an easy workaround if you inherit from UICollectionViewFlowLayout:
-(CGSize)collectionViewContentSize { //Workaround
CGSize superSize = [super collectionViewContentSize];
CGRect frame = self.collectionView.frame;
return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame)));
}
2019 actual solution
Say you have a number of layouts for your "Cars" view.
Let's say you have three.
CarsLayout1: UICollectionViewLayout { ...
CarsLayout2: UICollectionViewLayout { ...
CarsLayout3: UICollectionViewLayout { ...
It will jump when you animate between layouts.
It's just an undeniable mistake by Apple. It jumps when you animate, without question.
The fix is this:
You must have a global float, and, the following base class:
var avoidAppleMessupCarsLayouts: CGPoint? = nil
class FixerForCarsLayouts: UICollectionViewLayout {
override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
avoidAppleMessupCarsLayouts = collectionView?.contentOffset
}
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
if avoidAppleMessupCarsLayouts != nil {
return avoidAppleMessupCarsLayouts!
}
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
So here are the three layouts for your "Cars" screen:
CarsLayout1: FixerForCarsLayouts { ...
CarsLayout2: FixerForCarsLayouts { ...
CarsLayout3: FixerForCarsLayouts { ...
That's it. It now works.
Incredibly obscurely, you could have different "sets" of layouts (for Cars, Dogs, Houses, etc.), which could (conceivably) collide. For this reason, have a global and a base class as above for each "set".
This was invented by passing user #Isaacliu, above, many years ago.
A detail, FWIW in Isaacliu's code fragment, finalizeLayoutTransition is added. In fact it's not necessary logically.
The fact is, until Apple change how it works, every time you animate between collection view layouts, you do have to do this. That's life!
This issue bit me as well and it seems to be a bug in the transition code. From what I can tell it tries to focus on the cell that was closest to the center of the pre-transition view layout. However, if there doesn't happen to be a cell at the center of the view pre-transition then it still tries to center where the cell would be post-transition. This is very clear if you set alwaysBounceVertical/Horizontal to YES, load the view with a single cell and then perform a layout transition.
I was able to get around this by explicitly telling the collection to focus on a specific cell (the first cell visible cell, in this example) after triggering the layout update.
[self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES];
// scroll to the first visible cell
if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) {
NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0];
[self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}
Jumping in with a late answer to my own question.
The TLLayoutTransitioning library provides a great solution to this problem by re-tasking iOS7s interactive transitioning APIs to do non-interactive, layout to layout transitions. It effectively provides an alternative to setCollectionViewLayout, solving the content offset issue and adding several features:
Animation duration
30+ easing curves (courtesy of Warren Moore's AHEasing library)
Multiple content offset modes
Custom easing curves can be defined as AHEasingFunction functions. The final content offset can be specified in terms of one or more index paths with Minimal, Center, Top, Left, Bottom or Right placement options.
To see what I mean, try running the Resize demo in the Examples workspace and playing around with the options.
The usage is like this. First, configure your view controller to return an instance of TLTransitionLayout:
- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}
Then, instead of calling setCollectionViewLayout, call transitionToCollectionViewLayout:toLayout defined in the UICollectionView-TLLayoutTransitioning category:
UICollectionViewLayout *toLayout = ...; // the layout to transition to
CGFloat duration = 2.0;
AHEasingFunction easing = QuarticEaseInOut;
TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil];
This call initiates an interactive transition and, internally, a CADisplayLink callback that drives the transition progress with the specified duration and easing function.
The next step is to specify a final content offset. You can specify any arbitrary value, but the toContentOffsetForLayout method defined in UICollectionView-TLLayoutTransitioning provides an elegant way to calculate content offsets relative to one or more index paths. For example, in order to have a specific cell to end up as close to the center of the collection view as possible, make the following call immediately after transitionToCollectionViewLayout:
NSIndexPath *indexPath = ...; // the index path of the cell to center
TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter;
CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:#[indexPath] placement:placement];
layout.toContentOffset = toOffset;
Easy.
Animate your new layout and collectionView's contentOffset in the same animation block.
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil];
[self.collectionView setContentOffset:CGPointMake(0, -64)];
} completion:nil];
It will keep self.collectionView.contentOffset constant.
If you are simply looking for the content offset to not change when transition from layouts, you can creating a custom layout and override a couple methods to keep track of the old contentOffset and reuse it:
#interface CustomLayout ()
#property (nonatomic) NSValue *previousContentOffset;
#end
#implementation CustomLayout
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
CGPoint previousContentOffset = [self.previousContentOffset CGPointValue];
CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ;
}
- (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout
{
self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset];
return [super prepareForTransitionFromLayout:oldLayout];
}
- (void)finalizeLayoutTransition
{
self.previousContentOffset = nil;
return [super finalizeLayoutTransition];
}
#end
All this is doing is saving the previous content offset before the layout transition in prepareForTransitionFromLayout, overwriting the new content offset in targetContentOffsetForProposedContentOffset, and clearing it in finalizeLayoutTransition. Pretty straightforward
If it helps add to the body of experience: I encountered this problem persistently regardless of the size of my content, whether I had set a content inset, or any other obvious factor. So my workaround was somewhat drastic. First I subclassed UICollectionView and added to combat inappropriate content offset setting:
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setContentOffset:(CGPoint)contentOffset
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
}
- (void)setContentSize:(CGSize)contentSize
{
_declineContentOffset ++;
[super setContentSize:contentSize];
_declineContentOffset --;
}
I'm not proud of it but the only workable solution seems to be completely to reject any attempt by the collection view to set its own content offset resulting from a call to setCollectionViewLayout:animated:. Empirically it looks like this change occurs directly in the immediate call, which obviously isn't guaranteed by the interface or the documentation but makes sense from a Core Animation point of view so I'm perhaps only 50% uncomfortable with the assumption.
However there was a second issue: UICollectionView was now adding a little jump to those views that were staying in the same place upon a new collection view layout — pushing them down about 240 points and then animating them back to the original position. I'm unclear why but I modified my code to deal with it nevertheless by severing the CAAnimations that had been added to any cells that, actually, weren't moving:
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
// collect up the positions of all existing subviews
NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary];
for(UIView *view in [self subviews])
{
positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]];
}
// apply the new layout, declining to allow the content offset to change
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
// run through the subviews again...
for(UIView *view in [self subviews])
{
// if UIKit has inexplicably applied animations to these views to move them back to where
// they were in the first place, remove those animations
CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:#"position"];
NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]];
if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue)
{
NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]];
if([targetValue isEqualToValue:sourceValue])
[[view layer] removeAnimationForKey:#"position"];
}
}
}
This appears not to inhibit views that actually do move, or to cause them to move incorrectly (as if they were expecting everything around them to be down about 240 points and to animate to the correct position with them).
So this is my current solution.
I've probably spent about two weeks now trying to get various layout to transition between one another smoothly. I've found that override the proposed offset is working in iOS 10.2, but in version prior to that I still get the issue. The thing that makes my situation a bit worse is I need to transition into another layout as a result of a scroll, so the view is both scrolling and transitioning at the same time.
Tommy's answer was the only thing that worked for me in pre 10.2 versions. I'm doing the following thing now.
class HackedCollectionView: UICollectionView {
var ignoreContentOffsetChanges = false
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
guard ignoreContentOffsetChanges == false else { return }
super.setContentOffset(contentOffset, animated: animated)
}
override var contentOffset: CGPoint {
get {
return super.contentOffset
}
set {
guard ignoreContentOffsetChanges == false else { return }
super.contentOffset = newValue
}
}
override func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) {
guard ignoreContentOffsetChanges == false else { return }
super.setCollectionViewLayout(layout, animated: animated)
}
override var contentSize: CGSize {
get {
return super.contentSize
}
set {
guard ignoreContentOffsetChanges == false else { return }
super.contentSize = newValue
}
}
}
Then when I set the layout I do this...
let theContentOffsetIActuallyWant = CGPoint(x: 0, y: 100)
UIView.animate(withDuration: animationDuration,
delay: 0, options: animationOptions,
animations: {
collectionView.setCollectionViewLayout(layout, animated: true, completion: { completed in
// I'm also doing something in my layout, but this may be redundant now
layout.overriddenContentOffset = nil
})
collectionView.ignoreContentOffsetChanges = true
}, completion: { _ in
collectionView.ignoreContentOffsetChanges = false
collectionView.setContentOffset(theContentOffsetIActuallyWant, animated: false)
})
This finally worked for me (Swift 3)
self.collectionView.collectionViewLayout = UICollectionViewFlowLayout()
self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)

Resizing UITableView using ViewDeck

I am trying to mimic the sliding table view functionality found in apps like Facebook. I am currently using the Inferis/ViewDeck library. This works great as a slider but I can't figure out how to resize the table view to fit properly in the smaller space left by the slider without clipping.
Example:
I have seen several other SO posts with similar questions but none that concisely answer how to resize a table view. Any examples or explanation would be great.
For left oriented slider (i.e. slider that reveals a view to the left):
- (BOOL)viewDeckControllerWillOpenLeftView:(IIViewDeckController*)viewDeckController animated:(BOOL)animated
{
self.tableView.frame = (CGRect) { self.tableView.frame.origin.x,
self.tableView.frame.origin.y,
320 - self.viewDeckController.leftLedge,
self.tableView.frame.size.height };
return YES;
}
For right oriented slider:
- (BOOL)viewDeckControllerWillOpenRightView:(IIViewDeckController*)viewDeckController animated:(BOOL)animated
{
self.tableView.frame = (CGRect) { self.viewDeckController.rightLedge,
self.tableView.frame.origin.y,
320 - self.viewDeckController.rightLedge,
self.tableView.frame.size.height };
return YES;
}
It is not possible to resize a UITableViewController, but you can create a UITableView inside the UIViewController, and resize the UITableView.

Scrolling, zooming UIScrollView and interface orientation rotation. How to use autoresize and more

This should be a pretty common thing to do, but I haven't been able to get it to work exactly right.
I have rectangular content. It normally fits in 320x361: portrait mode minus status bar minus ad minus tab bar.
I have put that content in a UIScrollView and enabled zooming. I also want interface rotation to work. The content will always be a tall rectangle, but when zoomed users might want to see more width at a time and less height.
What do I need to do in Interface Builder and code to get this done? How should I set my autoresizing on the different views? How do I set my contentSize and contentInsets?
I have tried a ton of different ways and nothing works exactly right. In various of my solutions, I've had problems with after some combination of zooming, interface rotation, and maybe scrolling, it's no longer possible to scroll to the entire content on the screen. Before you can see the edge of the content, the scroll view springs you back.
The way I'm doing it now is about 80% right. That is, out of 10 things it should do, it does 8 of them. The two things it does wrong are:
When zoomed in portrait mode, you can scroll past the edge of the content, and see a black background. That's not too much to complain about. At least you can see all the content. In landscape mode, zoomed or not, seeing the black background past the edge is normal, since the content doesn't have enough width to fill the screen at 1:1 zoom level (the minimum).
I am still getting content stuck off the edge when it runs on a test device running iOS 3.0, but it works on mine running 4.x. -- Actually that was with the previous solution. My tester hasn't tried the latest solution.
Here is the solution I'm currently using. To summarize, I have made the scroll view as wide and tall as it needs to be for either orientation, since I've found resizing it either manually or automatically adds complexity and is fragile.
View hierarchy:
view
scrollView
scrollableArea
content
ad
view is 320x411 and has all the autoresizing options on, so conforms to screen shape
scrollView is 480 x 361, starts at origin -80,0, and locks to top only and disables stretching
scrollableArea is 480 x 361 and locks to left and top. Since scrollView disables stretching, the autoresizing masks for its subviews don't matter, but I tell you anyway.
content is 320x361, starts at origin 80,0, and locks to top
I am setting scrollView.contentSize to 480x361.
shouldAutorotateToInterfaceOrientation supports all orientations except portrait upside down.
In didRotateFromInterfaceOrientation, I am setting a bottom content inset of 160 if the orientation is landscape, resetting to 0 if not. I am setting left and right indicator insets of 80 each if the orientation is portrait, resetting if not.
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 2.0
viewForZoomingInScrollView returns scrollableArea
// in IB it would be all options activated
scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
scrollView.contentSize = content.frame.size; // or bounds, try both
what do you mean with scrollableArea?
your minZoomScale is set to 1.0 thats fine for portrait mode but not for landscape. Because in landscape your height is smaller than in portrait you need to have a value smaller than 1.0. For me I use this implementation and call it every time, the frame of the scrollView did change:
- (void)setMaxMinZoomScalesForCurrentBounds {
CGSize boundsSize = self.bounds.size; // self is a UIScrollView here
CGSize contentSize = content.bounds.size;
CGFloat xScale = boundsSize.width / contentSize.width;
CGFloat yScale = boundsSize.height / contentSize.height;
CGFloat minScale = MIN(xScale, yScale);
if (self.zoomScale < minScale) {
[self setZoomScale:minScale animated:NO];
}
if (minScale<self.maximumZoomScale) self.minimumZoomScale = minScale;
//[self setZoomScale:minScale animated:YES];
}
- (void)setFrame:(CGRect)rect { // again, this class is a UIScrollView
[super setFrame:rect];
[self setMaxMinZoomScalesForCurrentBounds];
}
I don't think I understood the entire problem from your post, but here's an answer for what I did understand.
As far as I know (and worked with UIScrollView), the content inside a UIScrollView is not automatically autoresized along with the UIScrollView.
Consider the UIScrollView as a window/portal to another universe where your content is. When autoresizing the UIScrollView, you are only changing the shape/size of the viewing window... not the size of the content in the other universe.
However, if needed you can intercept the rotation event and manually change your content too (with animation so that it looks good).
For a correct autoresize, you should change the contentSize for the scrollView (so that it knows the size of your universe) but also change the size of UIView. I think this is why you were able to scroll and get that black content. Maybe you just updated the contentSize, but now the actuall content views.
Personally, I haven't encountered any case that required to resize the content along with the UIScrollView, but I hope this will get you started in the right direction.
If I understand correctly is that you want a scrollview with an image on it. It needs to be fullscreen to start with and you need to be able to zoom in. On top of that you want it to be able to rotate according to orientation.
Well I've been prototyping with this in the past and if all of the above is correct the following code should work for you.
I left a bit of a white area for the bars/custombars.
- (void)viewDidLoad
{
[super viewDidLoad];
//first inits and allocs
scrollView2 = [[UIScrollView alloc] initWithFrame:self.view.frame];
imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"someImageName"]];
[scrollView2 addSubview:imageView];
[self drawContent]; //refreshing the content
[self.view addSubview:scrollView2];
}
-(void)drawContent
{
//this refreshes the screen to the right sizes and zoomscales.
[scrollView2 setBackgroundColor:[UIColor blackColor]];
[scrollView2 setCanCancelContentTouches:NO];
scrollView2.clipsToBounds = YES;
[scrollView2 setDelegate:self];
scrollView2.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[scrollView2 setContentSize:CGSizeMake(imageView.frame.size.width, imageView.frame.size.height)];
[scrollView2 setScrollEnabled:YES];
float minZoomScale;
float zoomHeight = imageView.frame.size.height / scrollView2.frame.size.height;
float zoomWidth = imageView.frame.size.width / scrollView2.frame.size.width;
if(zoomWidth > zoomHeight)
{
minZoomScale = 1.0 / zoomWidth;
}
else
{
minZoomScale = 1.0 / zoomHeight;
}
[scrollView2 setMinimumZoomScale:minZoomScale];
[scrollView2 setMaximumZoomScale:7.5];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
if (interfaceOrientation == UIInterfaceOrientationPortrait || interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
// Portrait
//the 88pxls is the white area that is left for the navbar etc.
self.scrollView2.frame = CGRectMake(0, 88, [UIScreen mainScreen].bounds.size.width, self.view.frame.size.height - 88);
[self drawContent];
}
else {
// Landscape
//the 88pxls is the white area that is left for the navbar etc.
self.scrollView2.frame = CGRectMake(0, 88, [UIScreen mainScreen].bounds.size.height, self.view.frame.size.width);
[self drawContent];
}
return YES;
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
I hope this will fix your troubles. If not leave a comment.
When you want to put a content (a UIView instance, let's call it theViewInstance ) in a UIScrollView and then scroll / zoom on theViewInstance , the way to do it is :
theViewInstance should be added as the subview of the UIScrollView
set a delegate to the UIScrollView instance and implement the selector to return the view that should be used for zooming / scrolling:
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return theViewInstance;
}
Set the contentSize of the UIScrollView to the frame of the theViewInstance by default:
scrollView.contentSize=theViewInstance.frame.size;
(Additionally, the accepted zoom levels can be set in the UIScrollView :)
scrollView.minimumZoomScale=1.0;
scrollView.maximumZoomScale=3.0;
This is the way a pinch to zoom is achieved on a UIImage : a UIImageView is added to a UIScrollView and in the UIScrollViewDelegate implementation, the UIImageView is returned (as described here for instance).
For the rotation support, this is done in the UIViewController whose UIView contains the UIScrollView we just talked about.

Can I override scroll view's automatic behavior to scroll to the first responder?

I have a UITextField inside a UIScrollView (a few levels deep). I am watching UIKeyboardDidShowNotification, and also calling the same code when I manually change the first responder (I might change to a different text field without momentarily hiding the keyboard). In that code I use scrollRectToVisible:animated: to make sure the UITextField is visible.
I was having a huge headache debugging why that was acting funny, but I realized now that UIScrollView automatically ensures that the first responder is within its bounds. I am changing the frame of the UIScrollView so that none of it is hidden behind the keyboard.
However, my code can be slightly smarter than their code, because I want to show not only the UITextField, but some nearby related views as well. I try to show those views if they will fit; if not whatever, I try to show as much of them as I can but at least ensure that the UITextField is visible. So I want to keep my custom code.
The automatic behavior interferes with my code. What I see is the scroll view gently scroll up so that the bottom edge of my content is visible, then it snaps down to where my code told it to position.
Is there anyway to stop the UIScrollView from doing its default capability of scrolling the first responder into view?
More Info
On reviewing the documentation I read that they advise to change the scroll view's contentInset instead of frame. I changed that and eliminated some unpredictable behavior, but it didn't fix this particular problem.
I don't think posting all the code would necessarily be that useful. But here is the critical call and the values of important properties at that time. I will just write 4-tuples for CGRects; I mean (x, y, width, height).
[scrollView scrollRectToVisible:(116.2, 71.2, 60, 243) animated:YES];
scrollView.bounds == (0, 12, 320, 361)
scrollView.contentInset == UIEdgeInsetsMake(0, 0, 118, 0)
textField.frame == (112.2, 222.6, 24, 24)
converted to coordinates of the immediate subview of scrollView == (134.2, 244.6, 24, 24)
converted to coordinates of scrollView == (134.2, 244.6, 24, 24)
So the scroll view bottom edge is really at y == 243 because of the inset.
The requested rectangle extends to y == 314.2.
The text field extends to y == 268.6.
Both are out of bounds. scrollRectToVisible is trying to fix one of those problems. The standard UIScrollView / UITextField behavior is trying to fix the other. They don't come up with quite the same solution.
I didn't test this particular situation, but I've managed to prevent a scrollview from bouncing at the top and bottom by subclassing the scrollview and overriding setContentOffset: and setContentOffset:animated:. The scrollview calls this at every scroll movement, so I'm fairly certain they will be called when scrolling to the textfield.
You can use the delegate method textFieldDidBeginEditing: to determine when the scroll is allowed.
In code:
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
self.blockingTextViewScroll = YES;
}
-(void)setContentOffset:(CGPoint)contentOffset
{
if(self.blockingTextViewScroll)
{
self.blockingTextViewScroll = NO;
}
else
{
[super setContentOffset:contentOffset];
}
}
-(void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if(self.blockingTextViewScroll)
{
self.blockingTextViewScroll = NO;
}
else
{
[super setContentOffset:contentOffset animated:animated];
}
}
If your current scroll behaviour works with a setContentOffset: override, just place it inside the else blocks (or preferably, in a method you call from the else blocks).
In my project I have succeeded to achieve this by performing my scroll only after some delay.
- (void)keyboardWillShow:(NSNotification *)note
{
NSDictionary *userInfo = note.userInfo;
CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIEdgeInsets contentInsets = self.tableView.contentInset;
contentInsets.bottom += keyboardFrame.size.height;
[self.tableView setContentInset:contentInsets];
[self performSelector:#selector(scrollToEditableCell) withObject:nil afterDelay:0];
}
Also there is other possibility to make your view with additional views to be first responder and fool scroll view where to scroll. Haven't tested this yet.
This may turn out to be useless, but have you tried setting scrollView.userInteractionEnabled to NO before calling scrollrectToVisible: & then setting it back to YES? It may prevent the automatic scrolling behavior.
Try changing the view autoresizing to UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin. The default is FlexibleTopMargin so maybe thats the reason. btw scrollRectToVisible: is using the scrollView.contentSize.
The other thing you can try to change the scrollView size first and then apply the scrollRectToVisible: change. First frame change, then content change. (Maybe observe the keyboard did appear event)
The automatic scrolling behavior seems to be especially buggy starting in iOS 14. I alleviated the problem by subclassing UIScrollView and overriding setContentOffset to do nothing. Here is the bases of my code.
class ManualScrollView: UIScrollView {
/// Use this function to set the content offset. This will forward the call to
/// super.setContentOffset(:animated:)
/// - Parameters:
/// - contentOffset: A point (expressed in points) that is offset from the content view’s origin.
/// - animated: true to animate the transition at a constant velocity to the new offset, false to make the transition immediate.
func forceContentOffset(_ contentOffset: CGPoint, animated: Bool) {
super.setContentOffset(contentOffset, animated: animated)
}
/// This function has be overriden to do nothing to block system calls from changing the
/// content offset at undesireable times.
///
/// Instead call forceContentOffset(:animated:)
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
}
}
This works but you have to deal with reimplementing many of the scroll views behaviors and methods that you normally get for free. Since scrollRectToView and scrollToView both use setContentOffset you also have to reimplement these if you want them to work.

UIScrollView: single tap scrolls it to top

I have the UIScrollView with pagingEnabled set to YES, and programmatically scroll its content to bottom:
CGPoint contentOffset = scrollView.contentOffset;
contentOffset.y = scrollView.contentSize.height - scrollView.frame.size.height;
[scrollView setContentOffset:contentOffset animated:YES];
it scrolls successfully, but after that, on single tap its content scrolls up to offset that it has just before it scrolls down. That happens only when I programmaticaly scroll scrollView's content to bottom and then tap. When I scroll to any other offset and then tap, nothing is happened.
That's definitely not what I'd like to get. How that should be fixed?
Much thanks in advance!
Timur.
This small hack prevents the UIScrollView from scrolling when tapped. Looks like this is happening when the scroll view has paging enabled.
In your UIScrollView delegate add this method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
scrollView.pagingEnabled = self.scrollView.contentOffset.x < (self.scrollView.contentSize.width - self.scrollView.frame.size.width);
}
This disables the paging when the scroll view reaches the right end in horizontal scrolling (my use case, you can adapt it to other directions easily).
I just figured out what causes this problem, and how to avoid it. If you having pagingEnabled set to YES on a scroll view, you must set the contentOffset to be a multiple of the scroll view's visible size (i.e. you should be on a paging boundary).
Concrete example:
If your scroll view was (say) 460 pixels high with a content area of 920, you would need to set the content offset to EITHER 0 or 460 if you want to avoid the "scroll to beginning on tap" problem.
As a bonus, the end result will probably look better since your scroll view will be aligned with the paging boundaries. :)
The following workaround did help (assume that one extends UIScrollView with a category, so 'self' refers to its instance):
-(BOOL) scrolledToBottom
{
return (self.contentSize.height <= self.frame.size.height) ||
(self.contentOffset.y == self.contentSize.height - self.frame.size.height);
}
Then, one should sometimes turn pagingEnabled off, just at the position where scroll view reaches its bottom. In the delegate (pagingEnabled is initialy on of course, since the problem occurs only when it is enabled):
-(void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.pagingEnabled == YES)
{
if ([scrollView scrolledToBottom] == YES)
scrollView.pagingEnabled = NO;
}
else
{
if ([scrollView scrolledToBottom] == NO)
scrollView.pagingEnabled = YES;
}
}
This seems to be a bug:
UIScrollView doesn't remember the position
I have tested this on iOS 4.2 (Simulator) and the issue remains.
When scrolling a ScrollView I would suggest using
[scrollView scrollRectToVisible:CGRectMake(0,0,1,1) animated:YES];
Where the rect is the position you're after. (In this case the rect would be the top of the scrollview).
Changing the content offset is not the correct way of scrolling a scrollview.