dismissModalViewControllerAnimated resets contentOffset - ios

I have a problem with my table view. When dismissing a modal view controller presented on top of it, it always scrolling to the top . I have tried observing the changes to contentOffset using KVO, but the one that messes my view goes behind it.
From the UITableViewController, when user finishes his task in the modal dialog, self.tableView.contentOffset is , I call:
[self dismissModalViewControllerAnimated:YES]
Subsequently, when the viewWillAppear:(BOOL)animated is called, the self.tableView.contentOffset is already set to 0,0.
Is this supposed to be happening? I am able to work around the issue by remembering the scroll position before presenting the modal view and restore it back in viewWillAppear after dismissing the modal view. But it seems wrong. Am I missing something?
I have found similar problem described in Dismiss modal view changes underlying UIScrollView.

It looks like this is default behavior of UITableViewController. I tested it in very simple app and It worked exactly as you said. If you don't like it, use UIViewController instead.

Here is how I work around this problem, so that the table view maintains the original scroll position. In my subclass of UITableViewController I have added:
#property (assign) CGPoint lastScrollPosition;
Then in the implementation, I have overridden the following:
- (void)viewWillAppear:(BOOL)animated
{
self.tableView.contentOffset = self.lastScrollPosition;
}
- (void)dismissModalViewControllerAnimated:(BOOL)animated
{
self.lastScrollPosition = self.tableView.contentOffset;
[super dismissModalViewControllerAnimated:animated];
}
If you want your table to initially appear scrolled to non-zero position, as I did, don't forget to initialize the lastScrollPosition in your viewDidLoad.

Related

How to rotate parent view when child view orientation changes

I'm having a problem adjusting a parent view's layout when orientation changes for it's child view. I have a collection view controller that, when one of the cells are tapped, pushes a child view on top. If an orientation change occurs while the child view is visible and it is dismissed, the parent view's collection view cells haven't adjusted for the new width.
I should note that this works fine if the parent view is visible.
The only thing that has fixed this for me is in the viewDidAppear method of the parent view controller invalidates the collection view layout, but for me it's too late as the user sees the animation of the collection view cells snap into place.
- (void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
}
I would have preferred to use viewWillAppear, but that doesn't seem to do anything. It sounds like it can only adjust the cells when the parent view is visible.
Is there a way around this?
Referring to this answer, iOS does not send orientation change events to offscreen view controllers, making them an unreliable way to determine whether the view has been resized.
viewWillAppear: isn't working in your case because iOS doesn't resize the offscreen view controller's view until after it calls the method, so your invalidate and reload are being pulled off the wrong values.
I believe the iOS8+ viewWillTransitionToSize:withTransitionCoordinator: method fires even when offscreen, but I'm not positive. In my experience, the size it provides does not reflect the actual size of the view. What I personally like to hook into is viewWillLayoutSubviews, usually guarded with a width check:
#property (nonatomic) CGFloat lastWidth;
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
if (self.lastWidth != CGRectGetWidth(self.view.bounds)) {
self.lastWidth = CGRectGetWidth(self.view.bounds);
// Update your collection view here.
}
}
This way, whenever your view is going to resize (on display, inside an orientation change animation) you can update the size information.
try overriding -(void)viewWillLayoutSubviews: method in your parent view controller. In your case,it goes like this
-(void)viewWillLayoutSubviews {
[self.collectionView.collectionViewLayout invalidateLayout];[self.collectionView reloadData];
}
You could try invalidating your layout and reloading in the rotation handler methods.
For pre-iOS 8
willRotateToInterfaceOrientation:duration:
And for iOS 8+
viewWillTransitionToSize:withTransitionCoordinator:

Navigation bar title bug with interactivePopGestureRecognizer

I am having a weird problem with UINavigationBar's title in an app when interactivePopGestureRecognizer comes into play. I have made a demo app to showcase this bug.
Setup:
The rootViewController is a UINavigationController.
FirstViewController has the navigation bar hidden, and interactivePopGestureRecognizer.enabled = NO;
Second and ThirdViewControllers have the navigation bar visible and the popgesture enabled.
Bug:
The bug occurs when going back from the Second to the First view using the popgesture. If you pull the second view halfway and then go back to the second view, the navigation title will show "Second View" (as expected).But when you go to the Third view, the title will not change to "Third View". And then on clicking the back button of the Third view, the navbar will get messed up.
Please check out my demo app. Any help explaining why this bug is happening will be appreciated. Thanks!
Remove Red Herrings
First of all, your example can be greatly simplified. You should delete all the viewDidLoad stuff, as it is a complete red herring and just complicates the issue. You should not be playing around with the pop gesture recognizer delegate on every change of view controller; and turning the pop gesture recognizer off and on is irrelevant to the example (it is on by default, and should just be left on for this example). So delete this kind of thing in all three view controllers:
- (void)viewDidLoad {
[super viewDidLoad];
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
self.navigationController.interactivePopGestureRecognizer.delegate = self;
}
}
(Don't delete the code that sets self.title, though you could have made things even simpler by doing that in the xib file for each view controller.)
You can also get rid of other unused methods throughout, such as the init... methods and memory alert methods.
Another issue, by the way, is that you have forgotten to call super in your implementations of viewWillAppear:. It is required that you do this. I don't think that affects the bug, but it is well to obey all the rules before you start trying to track these things down.
Now the bug still happens but we have much simpler code, so we can start to isolate the issue.
How The Pop Gesture Works
So what's the cause of the problem? I think the most obvious way to understand it is to realize how the pop gesture works. This is an interactive view controller transition animation. That's right - it's an animation. The way it works is that the pop animation (slide from the left) is attached to the superview layer, but with a speed of 0 so that it doesn't actually run. As the gesture proceeds, the timeOffset of the layer is constantly being updated, so that the corresponding "frame" of the animation appears. Thus it looks like you are dragging the view, but you are not; you are just making a gesture, and animation is proceeding at the same rate and to the same degree. I have explained this mechanism in this answer: https://stackoverflow.com/a/22677298/341994
Most important (pay attention to this part), if the gesture is abandoned in the middle (which it almost certainly will be), a decision is made as to whether the gesture is more than half-way completed, and based on this, either the animation is rapidly played to the end (i.e. the speed is set to something like 3) or the animation is run backwards to the start (i.e. the speed is set to something like -3).
Solutions And Why They Work
Now let's talk about the bug. There are two complications here that you've accidentally banged into:
As the pop animation and pop gesture begin, viewWillAppear: is called for the previous view controller even though the view may not ultimately appear (because this is an interactive gesture and the gesture may be cancelled). This can be a serious issue if you are used to the assumption that viewWillAppear: is always followed by the view actually taking over the screen (and viewDidAppear: being called), because this is a situation in which those things might not happen. (As Apple says in the WWDC 2013 videos, "view will appear" actually means "view might appear".)
There is a secondary set of animations, namely, everything connected with the navigation bar - the change of title (it is supposed to fade into view) and, in this case, the change between not hidden and hidden. The runtime is trying to coordinate the secondary set of animations with the sliding view animation. But you have made that difficult by calling for no animation when the bar is hidden or shown.
Thus, as you've already been told, one solution is to change animated:NO to animated:YES throughout your code. This way, the showing and hiding of the navigation bar is ordered up as part of the animation. Therefore, when the gesture is cancelled and the animation is run backwards to the start, the showing/hiding of the navigation is also run backwards to the start - the two things are now staying coordinated.
But what if you really don't want to make that change? Well, another solution is to change viewWillAppear: to viewDidAppear: throughout. As I've already said, viewWillAppear: is called at the start of the animation, even if the gesture won't be completed, which is causing things to get out of whack. But viewDidAppear: is called only if the gesture is completed (not canceled) and when the animation is already over.
Which of those two solutions do I prefer? Neither of them! They both force you to make changes you don't want to make. The real solution, it seems to me, is to use the transition coordinator.
The Transition Coordinator
The transition coordinator is an object supplied by the system for this very purpose, i.e., to detect that we're involved in an interactive transition and to behave differently depending on whether it is canceled or not.
Concentrate just on the OneViewController implementation of viewWillAppear:. This is where things are getting messed up. When you're in TwoViewController and you start the pan gesture from the left, OneViewController's viewWillAppear: is being called. But then you cancel, letting go of the gesture without completing it. In just that one case, you want not to do what you were doing in OneViewController's viewWillAppear:. And that is exactly what the transition coordinator allows you to do.
Here, then, is a rewrite of OneViewController's viewWillAppear:. This fixes the problem without your having to make any other changes:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
id<UIViewControllerTransitionCoordinator> tc = self.transitionCoordinator;
if (tc && [tc initiallyInteractive]) {
[tc notifyWhenInteractionEndsUsingBlock:
^(id<UIViewControllerTransitionCoordinatorContext> context) {
if ([context isCancelled]) {
// do nothing!
} else { // not cancelled, do it
[self.navigationController setNavigationBarHidden:YES animated:NO];
}
}];
} else { // not interactive, do it
[self.navigationController setNavigationBarHidden:YES animated:NO];
}
}
The fix is simple , but I don't have any explanation at the moment why this is happening.
One your OneViewController change your viewWillAppear to ,
-(void)viewWillAppear:(BOOL)animated{
// [self.navigationController setNavigationBarHidden:YES animated:NO];
self.navigationController.navigationBar.hidden = YES;
}
and on the second and third view controllers change it to,
-(void)viewWillAppear:(BOOL)animated{
//[self.navigationController setNavigationBarHidden:NO animated:NO];
self.navigationController.navigationBar.hidden = NO;
}
Strange but this will fix the issue when we directly use the hidden property of the UINavigationBar.
I don't know how do you make "FirstViewController has the navigation bar hidden".
I have the same problem, and I fixed it by replacing
self.navigationController.navigationBarHidden = YES / NO;
by
[self.navigationController setNavigationBarHidden:YES / NO animated:animated];
I gave up trying to make this work used my own swipe recognizer that pops the navigation stack:
override func viewDidLoad() {
super.viewDidLoad()
// disable system swipe back gesture and add our own
navigationController?.interactivePopGestureRecognizer?.enabled = false
let swipeBackGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipeBackAction:")
swipeBackGestureRecognizer.direction = UISwipeGestureRecognizerDirection.Right
tableView.addGestureRecognizer(swipeBackGestureRecognizer)
}
func swipeBackAction(sender: UISwipeGestureRecognizer) {
navigationController?.popViewControllerAnimated(true)
}
Disable the system interactivePopGestureRecognizer
Create your own UISwipeGestureRecognizer with a Right direction
Pop the navigation stack animated when he swipe is detected
Here's what fixed it for me (Swift)
1st view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
2nd and 3rd view controllers:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}

UIView Animation Fails

So, I want to do some basic animations of labels and later views.
I have a label, I'm trying to get it to move when a view loads, so I call the following method at the end of viewDidLoad:
- (void)animateView {
NSLog(#"animateView");
[UIView animateWithDuration:20 animations:^{
// set new position of label which it will animate to
self.dcFirstRunDaysLabel.frame = CGRectMake(20,320,280,215);
}];
}
Instead of animating, the label appears in position.
I've tried every tutorial and read through the docs. I get no errors.
Any thoughts?
Cheers.
Try calling your animateView method in viewDidAppear. Because in viewDidLoad your view isn't visible yet.
viewDidLoad:
Called after the controller’s view is loaded into memory.
viewDidAppear:
Notifies the view controller that its view was added to a view hierarchy.

how to make a custom scroll container view controller?

There are two view controllers in my app, e.g, vc1 and vc2. The two view controllers are as the subviews of a scrollView, so the user can scroll the screen to switch the view. However, the simple implement has a problem: the viewWillAppear method of vc1 and vc2 is called only once. so I want to implement my scroll container view controller, which can call viewWillAppear method correctly, please tell me how to implement it.
I am not sure what you are trying to do, but I think a simple UITableView or UICollectionView may be better for you because they have datasource method that will automatically called when a view will show up in the screen. You can update your two views when you need to return a UITableViewCell or UICollectionViewCell.
I'm not sure if this will work, but I'm thinking you can check if the vc1 and vc2's frames are withing the screen's bounds in the delegate method of the scrollView.
I'm pretty sure there's a method being called every time the scrollView is being scrolled. In this method, you can check
//put this in your .h or something
BOOL vc1IsVisible = true;
//in the scrollView delegate-method that is called upon scrolling
if([self isInsideView:vc1])
{
if(!vc1IsVisible)
{
vc1IsVisible = true;
[vc1 viewDidAppear:NO]; //or whatever it is for animation
}
}
else
{
if(vc1IsVisible)
vc1IsVisible = false
//and viewDidDisappear?
}
and then create a method somewhere like this
-(BOOL)isInsideView:(UIViewController*)vc
{
//Check if vc.origin.y is greater than scrollView.size.height or something maybe?
//You can probably also try using the scrollView's contentOffset and use that
//relative to the viewController's sizes.
//if the viewControllers bounds are withing the scrolls bounds, return YES;
//else, return NO;
}
Sorry I can't really test anything just now. Maybe I'll make something and update the answer later if you haven't figured it out. And you need to do it with both. I'm sure you can figure out a better way to include both in one method with this, or even with one variable.
Since you are using ViewController by adding it subview of scrollview, by adding ViewController this way viewDidLoad, viewWillAppear, viewDidAppear will be called only once, I mean there is no use of viewWillAppear here as such, rather if you want to update anything in the added ViewController you should create a public class in ViewController and call it when you need an update..

How to reset my UIScrollView's position after returning from a modal transition?

I have a simple view containing a long view with many buttons, with the whole thing being in a UIScrollView. The scroller works well, and I can scroll to the bottom and click a button. Every button triggers a modal seque to another view. That new view is then dismissed by user interaction, causing the original UIScrollView's view to load again.
Here's the problem: If I click on a button toward the top of the UIScrollView, I enter the modal segue, dismiss the new view, and return to the UIScrollView's view without a problem. But, if I click on one of the buttons toward the bottom of the UIScrollView, when I return seque out and then transition back, my scrolling is all messed up. I can only see the area beneath my scroller, and can't scroll back up to the top anymore!
I'm pretty sure there must be some way to reset the UIScrollView's starting and ending points upon ViewWillAppear, but I can't figure it out. Any help is appreciated!
Also, FYI, I simply added the UIScrollView through interface builder, and haven't implemented or synthesized it anywhere yet.
try this code:
- (void)viewWillAppear:(BOOL)animated
{
[yourscrollview setContentOffset:CGPointZero animated:YES];
}
Please note: the bug this question and answer is about appears to be fixed in iOS 7. The rest of this answer is only relevant to iOS 6 (and probably earlier).
The behaviour being exhibited here is a bug in the UIScrollView class. As noted by the OP, after returning from a modally presented UIViewController to a scene containing a UIScrollView, the UIScrollView takes whatever point it's currently scrolled to and starts behaving as though that is its origin. That means that if you'd scrolled down your scroll view before modally presenting another View Controller, you can't scroll back up upon returning to the scene with the scroll view.
The same thing happens when you remove the Scroll View from the view hierarchy and re-add it, even without changing its window.
You can work around this by setting the contentOffset of the scroll view back to {0,0} before it gets displayed again after dismissing the modal View Controller. If you actually want to preserve the point the user had scrolled to before they triggered the modal, then after the UIScrollView is redisplayed you can set the contentOffset back to whatever it was before you reset it.
Here's a UIScrollView subclass that fixes the bug without resetting the scroll view to the top whenever you return from a modal:
#interface NonBuggedScrollView : UIScrollView
#end
#implementation NonBuggedScrollView {
CGPoint oldOffset;
}
-(void)willMoveToWindow:(UIWindow *)newWindow {
oldOffset = self.contentOffset;
self.contentOffset = CGPointMake(0,0);
}
-(void)willMoveToSuperview:(UIView *)newSuperview {
oldOffset = self.contentOffset;
self.contentOffset = CGPointMake(0,0);
}
-(void)didMoveToWindow {
self.contentOffset = oldOffset;
}
-(void)didMoveToSuperview {
self.contentOffset = oldOffset;
}
#end
If you'd rather do this in a UIViewController than in a UIScrollView subclass, change the content offset in the viewWillAppear: and viewDidAppear methods.
If you don't want to preserve where the user's scroll position when they return from a modal, and just want to scroll the UIScrollView back to the top, as the OP asked for, then all you need is the even simpler:
#interface NonBuggedScrollView : UIScrollView
#end
#implementation NonBuggedScrollView
-(void)willMoveToWindow:(UIWindow *)newWindow {
self.contentOffset = CGPointMake(0,0);
}
-(void)willMoveToSuperview:(UIView *)newSuperview {
self.contentOffset = CGPointMake(0,0);
}
#end
First, thanks for the approved answer above. Someone mentioned that it was no longer applicable but I have a scrolling view inside of table view cell and it needs to be reset when the cell is reused.
Here is the solution in Swift.
#IBOutlet var scrollView: UIScrollView!
// many lines of code later inside a function of some sort...
scrollView.setContentOffset(CGPointMake(0.0, 0.0), animated: false)
To solve this problem i use this code:
-(void) viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self.scrollview scrollRectToVisible:CGRectMake(0, 0, 1, 1)
animated:NO];
}
You can change the starting and ending points by calling scrollRectToVisible:animated:. But I'm not sure if this fixes your problem.
Use below code snippet to restore the scroll position for a UIScrollview
Declare "scrollPosition" variable as CGPoint type.
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
//get the current offset
scrollPosition = scrollView.contentOffset;
//set current view to the beginning point
self.scrollView.contentOffset = CGPointZero;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
//retrieve the previous offset
self.scrollView.contentOffset = scrollPosition;
}

Resources