You can see on the gif below that on the first scroll of UITableView cell's content moves a tiny bit. You can barely see it, margins become 1 pixel wider.I've never encountered this before. It seems like there's some layout issue before the first scroll and it resolves itself after the fact. There's no warning in XCode, these custom cells are pretty straightforward, with no layout code overrides.
I don't know where to start, how do I catch this glitch?
UPDATE. I've implemented an obvious workaround for now:
- (void)scrollTableToFixGlitch {
[self.tableView setContentOffset:CGPointMake(0, 1)];
[self.tableView setContentOffset:CGPointMake(0, -1)];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self scrollTableToFixGlitch];
}
Still looking into the problem. I've tried generic UITableViewCells, nothing changed. Seems like it's the problem with View Controller's root view or tableview layout, and not with the table cells.
UPDATE 2.
I ruled out all the animations out of the question, problem lies somewhere in a different region. The glitch is easy to recreate on a much simplified project. My Tab Bar controller is based on MHCustomTabBarController with custom segues and some other additions. Here's what you do to recreate a glitch. Setup a project where your initial VC is embedded in Navigation Controller. The next controller either MHCustomTabBarController or a subclass is pushed to the navigation stack. First visible VC in tab bar is generic Table View Controller. That's it. Glitch appears only if tab bar controller is pushed in navigation stack.
Here's some code that I think matters from tab bar controller:
-(void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.childViewControllers.count < 1) {
[self performSegueWithIdentifier:#"viewController1" sender:[self.buttons objectAtIndex:0]];
}
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if (![segue isKindOfClass:[MHTabBarSegue class]]) {
[super prepareForSegue:segue sender:sender];
return;
}
self.oldViewController = self.destinationViewController;
//if view controller isn't already contained in the viewControllers-Dictionary
if (![self.viewControllersByIdentifier objectForKey:segue.identifier]) {
[self.viewControllersByIdentifier setObject:segue.destinationViewController forKey:segue.identifier];
}
[self.buttons setValue:#NO forKeyPath:#"selected"];
[sender setSelected:YES];
self.selectedIndex = [self.buttons indexOfObject:sender];
self.destinationIdentifier = segue.identifier;
self.destinationViewController = [self.viewControllersByIdentifier objectForKey:self.destinationIdentifier];
[[NSNotificationCenter defaultCenter] postNotificationName:MHCustomTabBarControllerViewControllerChangedNotification object:nil];
}
And a custom segue code:
#implementation MHTabBarSegue
- (void) perform {
MHCustomTabBarController *tabBarViewController = (MHCustomTabBarController *)self.sourceViewController;
UIViewController *destinationViewController = (UIViewController *) tabBarViewController.destinationViewController;
//remove old viewController
if (tabBarViewController.oldViewController) {
[tabBarViewController.oldViewController willMoveToParentViewController:nil];
[tabBarViewController.oldViewController.view removeFromSuperview];
[tabBarViewController.oldViewController removeFromParentViewController];
}
destinationViewController.view.frame = tabBarViewController.container.bounds;
[tabBarViewController addChildViewController:destinationViewController];
[tabBarViewController.container addSubview:destinationViewController.view];
[destinationViewController didMoveToParentViewController:tabBarViewController];
}
#end
UPDATE 3
During my research I've found that - viewWillAppear is not called the first time when child controller appears. But it's called in all subsequent times.
Maybe the scrollviews contentSize is wider than your scrollView's frame(width specifically in this case) causing scrolling for both directions.
You can either try to decrease the contentSize width to the scrollView's width and
self.scrollView.alwaysBounceHorizontal = NO;
If this doesn't work, the solution would be to disable horizontal scrolling programatically by the help of the UIScrollView delegate
self.scrollView.delegate = self;
[self.scrollView setShowsHorizontalScrollIndicator:NO];
//for the below UIScrollView delegate function to work do the necessary step in the bottom of my answer.
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.x > 0)
scrollView.contentOffset = CGPointMake(0, scrollView.contentOffset.y);
}
And in your .h file you should change the interface line to below by adding UIScrollViewDelegate
#interface ViewController : UIViewController <UIScrollViewDelegate>
You most probably know this delegate part very well but for others it might be needed:D
Original answer
Ah, I've finally found the origin of this behaviour. I was almost sure this is happening due to some of the preparation methods are not called properly. As I stated in the update 3 I've found that -viewWillAppear method is not called in TableViewController when my TabBarController is pushed to the navigation stacked. I've found a ton of coverage of this matter on SO, it's a very well known issue apparently.
I've added a simple fix just to check if I'm right in my Custom Segue:
//added this line
[destinationViewController viewWillAppear:YES];
[tabBarViewController.container addSubview:destinationViewController.view];
And it works like a charm, no flickering! Now I have to figure out a more suitable place for this call, since explicit calls to methods like this can break a lot of stuff.
Probably the best place is in -navigationController:willShowViewController:animated: method of UINavigationControllerDelegate.
Anyway, problem solved. Hope it will help someone with the same issue.
UPDATE Actually, I was not completely correct on that. -viewWillAppear is called on my tab bar controller when it's pushed to navigation stack. It's not being translated to TableViewController. So there's no need to access NavigationControllerDelegate. Simple fix to a custom segue is enough.
Related
So, I have a setup as follows:
UITabBarController -> UINavigationController -> UITableViewController
The TabBarController has another view, but that's unimportant. I have the TabBar set to hidden because the user should not be doing the switching between views.
I created a demo project to reproduce the issue with exactly as I described above.
I called
self.tabBarController.tabBar.hidden = YES;
Inside the viewDidLoad method of the tableViewController and was, of course, left with this issue:
So, how did I end up solving hacking my problem?
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.tableView setContentInset:UIEdgeInsetsMake(self.tableView.contentInset.top, self.tableView.contentInset.left, 0, self.tableView.contentInset.right)];
[self.tableView setScrollIndicatorInsets:UIEdgeInsetsMake(self.tableView.contentInset.top, self.tableView.contentInset.left, 0, self.tableView.contentInset.right)];
}
I had to put it inside viewDidAppear because it didn't work in viewDidLoad.
So, does anyone know / can anyone think of a non-hacky way to solve this issue?
Here's a link to the project: https://www.dropbox.com/sh/a6dxecssl3csds7/AAC7xNhSEuawMWCk6hAvyQVYa?dl=0
I have a UIViewController subclass that contains a UICollectionView. Selecting a cell presents a new view controller. When I return to the first view controller, the contentOffset of the collection view is reset to CGPointZero.
From my research this seems to be standard behaviour.
I can reset the contentOffset by adding a private property to my view controller subclass, saving the contentOffset in the viewWillDissapear method and resetting it on the collection view in the viewWillAppear method.
I would like to know though, is there another way to prevent the scroll view content offset from being reset in the first place (removing the need for an extra property)?
I am targetting iOS7.
The 2nd view controller is presented like this:
[self presentViewController:secondVC animated:YES completion:nil];
And dismissed like this (in the 2nd view controller):
-(void) dismiss
{
[self dismissViewControllerAnimated:YES completion:nil];
}
Edit: After further investigation it appears resetting the contentOffset is not the default behaviour. I haven't figured out why it is happening in my application yet. I am presenting just as have showed in the code above.
There are 2 options. If your application works with UINavigationController, it should keep the last accessed cell, not sure about how you have worked with. If not, you can always create a notification and in the moment you are getting back to the previous screen, send the update of the UICollectionView or whatever you have and scroll to the correct position.
Bear in mind, the UIViewController life cycle and how it´s going to appear before to update the user interface.
You can create a CGPoint object in AppDelegate. Then you can set that value to your CollectionView objects contentOffset while present and dismiss like that;
[self presentViewController:secondVC animated:YES completion:{
appDelegate.tempContentValue = _collectionView.contentOffset;
}];
[self dismissViewControllerAnimated:YES completion:{
UIViewController *first = [UIViewController alloc] init];
[first.collectionView setContentOffset:appDelegate.tempContentValue animated:YES];
}];
#property (nonatomic,assign) CGPoint newContentOffset;
Then when you navigate to another View / present another view
self.newContentOffset = self.collectionView.contentOffset;
In viewWillAppear
[self.collectionView reloadData];
if (self.newContentOffset) {
[self performSelector:#selector(setContentOffset) withObject:nil afterDelay:0.1];
}
-(void)setContentOffset {
[self.collectionView setContentOffset:self.newContentOffset];
}
There will be a small jerk while applying this . But if thats ok, you can go ahead with this method
I have a UINavigationController (NC) containing a UITableViewController (TVC0). When the user taps a row, it loads a UIPageViewController (PVC), which pages back and forth between other UITableViewControllers (TVC1).
TVC0 shows up inside NC (meaning it doesn't hide behind the navigation bar at the top or the tab bar at the bottom). When it pushes PVC, the first TVC1 appears inside the bounds of the nav bar and tab bar. However when I swipe, the TVC1s inside are hidden behind the navigation bar and tab bar. I can pull to reveal the contents, but when I release, it snaps back to behind the bar.
How can I force everything to appear between the two bars? I can't use storyboard (because it's a legacy app) and the embed in... option isn't available.
[Edit]
I added some logging and discovered that my embedded TVC1s frame has an absolute origin of 0, 64, but as soon as I tap, it goes to 0, 0. If I can't figure out a real solution, I can always fake it by adding 64, but I'd much rather figure out what's actually wrong.
[/Edit]
[More Edit]
I was testing another area in the iOS 6 simulator and discovered that this paging works flawlessly in iOS 6. So the issue I'm seeing is iOS 7 specific.
[/More Edit]
Here is my TVC0 viewDidLoad, PVC pageViewController:viewControllerBeforeViewController:, and a helper viewControllerAtIndex::
- (void) viewDidLoad
{
[super viewDidLoad];
NSDictionary* options = [NSDictionary dictionaryWithObject:
[NSNumber numberWithInteger: UIPageViewControllerSpineLocationMin]
forKey:
UIPageViewControllerOptionSpineLocationKey];
self.pageController = [[UIPageViewController alloc] initWithTransitionStyle:
UIPageViewControllerTransitionStyleScroll
navigationOrientation:
UIPageViewControllerNavigationOrientationHorizontal
options: options];
self.pageController.dataSource = self;
self.pageController.view.frame = self.view.frame;
NSArray* viewControllers =
[NSArray arrayWithObject: [self viewControllerAtIndex: self.initialIndex]];
[self.pageController setViewControllers: viewControllers
direction: UIPageViewControllerNavigationDirectionForward
animated: NO
completion: nil];
[self addChildViewController: self.pageController];
[self.view addSubview: self.pageController.view];
[self.pageController didMoveToParentViewController: self];
for (UIGestureRecognizer* recognizer in self.pageController.gestureRecognizers)
{
if ([recognizer isKindOfClass: [UITapGestureRecognizer class]])
{
recognizer.enabled = NO;
}
}
}
// SearchResultsList is TVC1
- (SearchResultsList*) viewControllerAtIndex: (NSUInteger) index
{
if (index >= self.items.count)
{
return nil;
}
SearchResultsList* retVal = [[SearchResultsList alloc]
initWithNibName: #"SearchResultsList" bundle: nil];
MyListItem* myItem = [self.items objectAtIndex: index];
MyMatchesRequest* matches = [[MyMatchesRequest alloc] initWithItemId: myItem.itemId];
[matches execute: ^(MySearchResults* results)
{
retVal.tableData = [NSMutableArray arrayWithArray: results.items];
retVal.view.frame = self.view.frame;
retVal.myItem = myItem;
retVal.index = index;
self.title = myItem.displayText;
[[retVal tableView] reloadData];
}];
return retVal;
}
- (UIViewController*) pageViewController: (UIPageViewController*) pageViewController
viewControllerBeforeViewController: (UIViewController*) viewController
{
SearchResultsList* vc = (SearchResultsList*)viewController;
if (vc.index == 0)
{
[self.navigationController popViewControllerAnimated: YES];
return nil;
}
return [self viewControllerAtIndex: vc.index - 1];
}
I had a very painful learning experience with similar behavior :(
Put this in your view controller's init:
self.automaticallyAdjustsScrollViewInsets = NO;
This is a new UIViewController property that defaults to YES in iOS 7
UIViewController Docs
Because you're presenting view controllers in a container which is less than the full size of the screen you need to set
self.pageViewController.definesPresentationContext = YES;
viewControllerWhichIsApageInPageController.modalPresentationStyle = UIModalPresentationCurrentContext
You code seems a little confused. You say it is your Page View Controller's viewDidLoad, yet it creates a new PVC and adds it as a child view controller to itself as parent. If you are really doing this, you are creating a PVC inside another PVC. I doubt this is what you want.
This code really belongs in TVC0 which invokes the PVC when the user taps a row. This invocation wouldn't be correct in viewDidLoad, but might sit nicely in a didSelectRowAtIndexPath method. Instead of bringing it in as a child controller, we can simply push it onto our navigationController's stack. (I expect you are doing this anyway in your outermost PVC).
But just in case, I would remove these three lines:
[self addChildViewController: self.pageController];
[self.view addSubview: self.pageController.view];
[self.pageController didMoveToParentViewController:self];
and replace them with
[self.navigationController pushViewController:self.pageController
animated:YES];
(this is called from your TVC0 - all of the code you have shown can live in TVC0)
Then to prevent the behaviour you describe, when you create your UINavigationController it should suffice to set the translucent property of it's navigationBar to NO.
update
I have looked at this issue in sufficient detail to see some buggy behaviour as you describe occur in some circumstances, but it's fairly hard to replicate.
The cause of the 'jump-up' behaviour is clear. If you have a translucent navBar, and it's automaticallyAdjustsScrollViewInsets is set to YES, in certain situations this can result in a private subview of the pageViewController (_UIQueuingScrollView) setting it's contentOffset.y to -64 when the pageViewController is first loaded. However as soon as the pageVC gets a chance to update itself (for example by a swipe or other touch gesture) it resets it's internal subviews state, losing that rogue contentOffset. So when you touch the first page, it jumps up. Where it then stays. This may be considered a bug, although it may be the result of misusing the pageViewController.
Just setting automaticallyAdjustsScrollViewInsets to NO doesn't cure your ills, as then your pages are all positioned behind the navBar. What you need to do is adjust the frame of the pageViewController itself.
I could go in to more detail, but at that point it gets hard to apply answers to your specific case as your app design looks a little odd, which is probably contributing to the issue.
if you can set your navigation controllers' navigation bar's translucent property to NO the problem should go away.
as I mentioned earlier, I cannot replicate your issue exactly as you describe, so I think you have not given a full and clear picture of your app design
loading a pageViewController as a child of a tableViewController, and making it's view a subview of the tableView, is a very odd way to go, and is doubtless contributing to your problems.
In viewControllerBeforeViewController the pageViewController child seems to pop it's parentViewController (the tableViewController) - so you would never see it's table contents? Either there is more relevant detail to the app, or you haven't described it accurately.
I recommend you first deal with these issues. Your problem may well then disappear.
I'm creating a slidingView controller using ECSlidingViewController like the one the previous version of Facebook had. Everything works fine but the menuViewController (tableViewController) is being overlapped by the status bar when I add an image to cell like shown below. Does anyone know how to fix it.
it's works 100%
- (void)viewDidLoad
{
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0")) {
[self.tableView setContentInset:UIEdgeInsetsMake(20, self.tableView.contentInset.left, self.tableView.contentInset.bottom, self.tableView.contentInset.right)];
}
[super viewDidLoad];
// Initialize table data
}
Use this to set tableview frame
[self.tableView setFrame:CGRectMake(self.tableView.frame.origin.x, 20, self.tableView.frame.size.width,self.tableView.frame.size.width)];
I am pushing self view to self.navigationcontroller by allocating. I have a tableView on that view so I am changing the content of tableview. But when I am pressing back button (that is automatically created), I am not able to show previous content. Its showing updated content.
Please suggest.
You set code in viewWillAppear method
- (void)viewWillAppear:(BOOL)animated
{
//code set here
}
If you fill the tableView's data in viewWillAppear: or viewDidAppear:, it will reload even if you only press the back button of your top viewController. If you do not want to have your content changed, you are supposed to use initWithNibName: or viewDidLoad: methoads. They are called only at creation time of the view.
Based on the comments on #Kirti's post, You can check if your viewcontroller is being popped by following method, and take some necessary actions for you controller holding table.
-(void) viewWillDisappear:(BOOL)animated
{
if(![self.navigationController.viewControllers containsObject:self])
{
YourControllerWithTable *instance = [self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count - 1];
instance.loadOldContent = YES;
}
}
In viewWillAppear: of YourControllerWithTable, you can check:
-(void) viewWillAppear:(BOOL)animated
{
if(loadOldContent)
{
//Do your work here
}
}
You don't push UIView instances onto a UINavigationController instance, only instances of UIViewController.