I'm currently debugging an issue in our app with a UITextView getting its last few lines cut off. The UITextView is inside of a collectionView cell that allows itself to be collapsed. I'm trying to catch the execution of the hiding of the text when its frame changes, so I set a symbolic breakpoint on -[UITextView setFrame:], and this will pause a ton of times during setup of the collectionView as expected.
However, when I hit the minimize button, the textView's frame somehow changes it's origin and height, but I never get a call to setFrame: (or at least my breakpoint never gets hit.) I also never get a call to setFrame: when I expand it, when again the origin and height changes.
The only code I call which manipulates the textView directly is hiding and unhiding it. After hiding, I then call the following:
- (void)updateCollection {
[self.collection performBatchUpdates:nil completion:nil];
[self.amountCell updateTable];
[self.collection setNeedsLayout];
[self.collection layoutIfNeeded];
}
The amountCell is unrelated to the cell I'm dealing with, so I don't think it has anything to do with this. Somehow, between hiding the UITextView and then calling a few updating methods on the collectionView, the frame changes without actaully calling setFrame:. How is this possible? How else could the frame be manipulated? Am I missing something obvious here?
One place you might look is in the collection view's -layoutSubviews method, which will be called by the framework whenever it determines that the layout needs to be updated, which you are hinting that it does when you invoke -setNeedsLayout and -layoutIfNeeded. It's an often-overlooked "hidden" influencer, especially with later versions of iOS shifting focus more to view controllers and away from views.
Related
I'm trying to optimize the UITableViewController for slower devices, and after eliminating many of the reasons why a tableview might be slow, I'm still trying to figure out the last one...
This screenshot is from instruments, showing the most expensive calls, during a time where no call is made to get heightForRowAtIndexPath or cellForRowAtIndexPath (scrolling ~10px up and down repeatedly without causing any cell to go out of the table, or get rendered).
Is this normal behavior? Why would the view need relayout if the content of the page is not changing?
Every time you scroll, you change the contentOffset. contentOffset updates bounds, specifically its origin. Every bounds change marks receiver for layout need in next update cycle. In this case to update frame of floating subviews like scroll indicator, table headers and footers, ... This is totally normal behaviour and there is nothing you can do about it. Don't worry, you are redisplaying, not redrawing.
We'd have to see some code to identify exactly why that method is being called but it could be from one of these reasons at least:
You have constraints that are being changed when scrolling even if you don't see it, they might be affected, maybe inside the UITableViewCell?
It does't have anything to do with the table view, the view just has something else to layout, maybe you made a constraint modification...
Your calling a method thats calls it explicitly.
Also remember this will only layout the view 'if needed'.
I have heard that viewDidLayoutSubviews is the best place to alter the layout when we have used constraints.
So I jumped to viewDidLayoutSubviews
I have created three UIViews and SubViewed them on SuperView. I am not mentioning the code for frame calculation.
[self.view addSubview:imgMiddleCircle];
[self.view addSubview:imgFirstCircle];
[self.view addSubview:imgLastCircle];
Using this piece of I am adding these circles.
Now when I run my code In viewDidLayoutSubviews I get following screens:
And when I switch to viewWillLayoutSubviews I am getting this on screen:
Why I am getting extra two circles in viewDidLayoutSubviews even I am creating three circles.
And why in viewWillLayout gives the correct Output.
You should code for the fact that viewDidLayoutSubviews is called multiple types.
Ideally addSubview: should be happening in a place like viewDidLoad where you are sure it is only called once.
You can create a flag to avoid calling addSubview: multiples types (not my personal choice)
Otherwise, try to move your set up code to viewDidLoad and force the view to render itself before doing your calculation:
[yourView setNeedsLayout];
[yourView layoutIfNeeded];
Per Apple Documentation,
When the bounds change for a view controller's view, the view
adjusts the positions of its subviews and then the system calls this
method. However, this method being called does not indicate that the
individual layouts of the view's subviews have been adjusted. Each
subview is responsible for adjusting its own layout.
Your view controller can override this method to make changes after
the view lays out its subviews. The default implementation of this
method does nothing.
So, essentially, viewDidLayoutSubiews gets called multiple times during the creation of your viewController including in cases like rotating the device, scrolling etc. So, you should be really careful with the code you add to this method, because it might be executed multiple times as well. Use this method to reposition your sub-views and not to add/remove them.
Take a look at this blog for more details.
The problem
In a custom view of mine (a UIScrollView subclass) I am calling setNeedsLayout in response to a "reload data" event (triggered by an external source). Most of the time this works correctly and layoutSubviews is called when the next view layout cycle occurs. Sometimes, however, layoutSubviews is not called! Up until now I was living with the "certain knowledge" that setNeedsLayout always triggers layoutSubviews. Apparently I was wrong. I even tried calling layoutIfNeeded after setNeedsLayout, but still no success.
The question
Obviously, I would like to solve my particular problem. On the other hand, I would like to improve my understanding of the view layout process on iOS, so I am formulating my question in a general way: Do you know of any conditions that can prevent layoutSubviews from being called after setNeedsLayout has been called? Answers that focus on UIScrollView are quite welcome, since that is where I am having trouble.
Problem context
I am on iOS 7.1, using Xcode 5.1.1. Notes on the implementation of my custom scroll view:
The scroll view has a single container view of type UIView that is always the same size as the scroll view content size
The scroll view's layoutSubviews implementation places custom subviews into the container view by manually calculating the subviews' frames
The custom subviews' implementation uses Auto Layout
Here is how the reloadData implementation looks like:
- (void) reloadData
{
// Iterates through an internal array that holds the subviews,
// then empties the array. Subviews are deallocated at this
// point.
[self removeAllSubviewsFromContainerview];
self.contentOffset = CGPointMake(0, 0);
// I verified that the content size is always greater than
// CGSizeZero (both width and height)
CGFloat contentWidth = [self calculateNewContentWidth];
self.contentSize = CGSizeMake(contentWidth, self.frame.size.height);
// Here is the problem: Sometimes this triggers layoutSubviews,
// sometimes it does not.
[self setNeedsLayout];
// Adding the following line for debugging purposes does not
// help, making it clear that setNeedsLayout has no effect.
// [self layoutIfNeeded];
}
Last but not least, here are some observations I made:
layoutSubviews is called as expected if the content offset or content size change. layoutSubviews is not called in my particular case if these two values don't change. I first assumed that this observation is a general working principle of UIScrollView, i.e. that layoutSubviews is generally not called for UIScrollView unless content offset or content size change. However, when I wrote a couple of minimal projects I failed to reproduce this assumed "general" behaviour - in those projects layoutSubviews was always called as expected, regardless of whether content offset or content size changed or not.
layoutSubviews is always called as expected if the scroll view displays dummy UILabel instances instead of my own custom subviews
layoutSubviews is always called as expected if I use my own custom subviews, but replace Auto Layout in the custom subviews' implementation by manual frame calculations
The last two observations have led me to believe that Auto Layout is somehow involved, but I really have no idea how this could affect how setNeedsLayout works.
In an iPhone word game I have an UIScrollView (holding UIImageView) and 7 draggable custom UIViews placed initially at the bottom (and outside the scroll view):
In the single ViewController.m I have overwritten viewDidLayoutSubviews so that it always sets the zoomScale of the scroll view - to have the UIImageView fill exactly 100% of the screen width.
This works well - for portrait and landscape modes. And when the app is just started:
My problem is however, when I first pinch/zoom/double-tap the scroll view and then move one of the seven Tile.m views:
Suddenly (not every time) viewDidLayoutSubviews is called after touchesBegan.
This resets the zoom of the scroll view - unexpectedly for the user.
My question is if there is any way to disable this behavior?
Is it possible to prevent viewDidLayoutSubviews call of the parent, when its child UIView is being touched/dragged?
UPDATE:
I've moved the zoomScale setting code from viewDidLayoutSubviews to didRotateFromInterfaceOrientation and the scroll view zoom is okay now, but its contentOffset is reset to {0,0}, when (not always) I drag a Tile - i.e. the scroll view jumps suddenly, the app is unusable.
Adding the following to viewDidLoad hasn't helped:
if ([self respondsToSelector:#selector(setAutomaticallyAdjustsScrollViewInsets:)])
self.automaticallyAdjustsScrollViewInsets = NO;
viewDidLayoutSubviews is called whenever the system performs layout on the view (layoutSubviews). This can be called for a plethora of reasons; you can subclass your view, implement an empty layoutSubviews method (don't forget to call the super implementation!) and put a breakpoint there to see who causes the layout. You may also want to implement setNeedsLayout and layoutIfNeeded, and put breakpoints there also for your investigation, as these trigger layout on followup runloops.
But you will never be able to prevent layout. The system performs layout on many occasions which are outside of your control. For example, if a user makes a call, exists the phone app and returns to your app; a layout is triggered because the bounds and frame of the window have changed. The call ends; layout is again triggered because now the window is back to previous size.
You should be responsible for figuring out when to set the zoom scale. For example, if the user starts a gesture, you should signal your code not to perform changes, even if a layout was performed.
Facing a really strange issue trying to dynamically add a left navigation panel to a View Controller, (should be able to support any view controller in the app, vaguely similar to the Facebook navigation) My idea seemed fairly simple, but I'm really not seeing where it's breaking down. What I've done is created a Category on UIViewController with the following method which I would think would move all the subviews to the right, and then add the new view.
-(void)addLeftView:(UIView *)newView
{
newView.frame=CGRectMake(0, 0, newView.frame.size.width, self.view.frame.size.height);
for(UIView *view in [self.view subviews])
{
view.frame=CGRectMake(view.frame.origin.x + newView.frame.size.width, view.frame.origin.y, view.frame.size.width, view.frame.size.height);
}
[self.view addSubview:newView];
}
What actually happens, though, is that the view is added, but the subviews do not move to the right. However, if you comment the addSubview out, everything actually does move to the right exactly as expected.
To make matters even weirder, if you wrap the view movement in a [UIView animateWithDuration:completionHandler:], where the completion handler adds the subview, the animation actually happens - all the views shift to the right, but when the subview gets added, they jump back to their starting position.
I assumed this was some sort of wacky auto-layout issue, so just to see what happened, I cleared all the constraints out of that view controller, and get the same result.
Found the answer in a similar but un-related thread. Can I disable autolayout for a specific subview at runtime?
Basically it was auto-layout reverting my positioning, so disabling it by setting this user variable on just the UIViews of problem fixed my issues.
You can set the translatesAutoresizingMaskIntoConstraints type Boolean, Value to Yes in the User Defined Runtime Attributes of the UIView you want in the xib/storyboard.