In UIViewController's lifecycle where is the best to set contentOffest of a UIScrollView?
If it is set in viewDidLoad or viewWillAppear, it has no effect. If in viewDidAppear then a small 'jump' will occur at beginning.
Tried with:
setContentOffset(CGPoint(x: some_value, y: 0), animated: false)
and:
contentOffset.x = some_value
I need to preload contentOffset with a previous scrolled position.
Anyway UIScrollView is a UICollectionView.
try to use it in viewDidLayoutSubviews
you should call viewWillAppearand viewDidLoad before attempting to set the offset.
However, it is possible you are trying to set the offset too early in the view lifecycle.
it might be good to override -(void)viewDidLayoutSubviews;, and set the offset there.
as your view's frames should all be set appropriately by that time.
Note: remember to call super there too
This answer is find from here.
Related
When I try to retrieve the safeAreaInsets in viewDidLoad in my view controller, it returns (0, 0, 0, 0) on the iPhone X, which I know is wrong. The documentation says that this will happen if the view is not part of the hierarchy or the view isn't visible. So my question is, where is the best place to retrieve the safeAreaInsets and then layout my subviews using these insets?
You need to use viewDidLayoutSubviews instead of viewDidLoad.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// your layout code here
}
Note: Unlike viewDidLoad which is only called once, viewDidLayoutSubviews is often called multiple times. Make sure to only set the frames inside viewDidLayoutSubviews, and don't perform operations that should only happen once, like adding subviews.
I have used autolayout constraints from storyboard. However in some cases, I want to calculate dynamic height of subview. I code this in viewDidAppear(), it works fine because this method is called after all view frames are set by layout constraints. The problem here is that I can see the frame set by constraints for half a second. And then the code reframes the view.
I came to know about viewDidLayout() which is called after constraints has set the frame so I can change. But it doesn't work. It is like this method is called before constraints are used.
The viewDidAppear method is called at the end of the view life cycle. So if you change the constraints here, it will always be visible.
If the change you want to do is a one time event, then you may do so in the method viewWillAppear. But this won't always help because the view drawing may not always be finished by this time. So a better option is to place the code in viewWillLayoutSubviews or viewDidLayoutSubviews.
NOTE: viewWillLayoutSubviews or viewDidLayoutSubviews will be called multiple times in a view's life cycle, such as orientation change or moving in and out of another view or any frame change for that matter. If the code change you want to put here is a one time event, then please make sure to use flags.
eg:-
- (void)viewWillLayoutSubviews {
if(firstTime) {
// put your constraint change code here.
}
}
Hope this helps! :)
As name suggests, viewDidLayoutSubviews is called when the view of your viewController has just finished its laying out and you can assume that at that dynamic height your views are in their correct places/frames according to your autolayout constraints.
Swift :
override func viewDidLayoutSubviews() {
// Set your constraint here
}
Objective C :
-(void)viewDidLayoutSubviews {
// Set your constraint here
}
I am not sure what you are actually trying to do in viewDidLayoutSubviews(). I always use to customise view by modifying layout constraint values overriding this method.
// Called to notify the view controller that its view has just laid out its subviews
override func viewDidLayoutSubviews() {
//Write your code here, any constraint modification etc.
super.viewDidLayoutSubviews()
}
I want to change a UITableViewCell's subview when it is being setup in the cellForRowAtIndexPath with the following code:
cellSubView.center = CGPointZero
Printing out the frame's coordinates shows, that the frame updates successfully, however the view is still displayed in the position that was given in the interface builder.
Overriding the viewDidAppear function with the following code would resolve this issue:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.tableView.reloadData()
}
But this will result in blinking: the table view already did appear when I change one of its cells' subview's position.
How is it possible to change the frame before the view did appear?
If you used autoLayout check if it's creating the constraints automatically.
If yes, then you can change the constants of these constraints instead.
If you are not using autoLayout, call layoutIfNeeded and see if that changes anything.
My bet though is on autoLayout creating constraints automatically.
I have a UIScrollView, and in viewDidAppear I set the contentSize:
- (void)viewDidAppear:(BOOL)animated{
CGSize scrollContentSize = CGSizeMake(320, 9200);
self.scrollView.contentSize = scrollContentSize;
}
This code is definitely running.
However, the view doesn't scroll. I wired up a button to log the contentSize, and it returns 0,0. If I get the button to set contentSize again it works fine.
I'm not referencing scrollView anywhere else in my code, what could be setting the contentSize back to 0, and is there any way I can stop it from doing so, or run my setup later in the process of setting up the view?
if you used the Autolayout then you have to make constraints for subviews in it. Then Autolayout will calculate its content size automatically.
But when I set contentsize through button action it works? How?
When the view relayout the scrollview again it will be calculated based on the constraints. So it won't work when u rotate the device/relayout the view.
I don't want to use the autolayout?Now what?
Then you are good to set contentsize.
One more thing, it is best practice to call super implementation on few lifecyle methods.
so do it so.
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Your code here
}
Here is the Good starting point
Excellent tutorial
I've got a a few UIScrollView on a page. You can scroll them independently or lock them together and scroll them as one. The problem occurs when they are locked.
I use UIScrollViewDelegate and scrollViewDidScroll: to track movement. I query the contentOffset of the UIScrollView which changed and then reflect change to other scroll views by setting their contentOffset property to match.
Great.... except I noticed a lot of extra calls. Programmatically changing the contentOffset of my scroll views triggers the delegate method scrollViewDidScroll: to be called. I've tried using setContentOffset:animated: instead, but I'm still getting the trigger on the delegate.
How can I modify my contentOffsets programmatically to not trigger scrollViewDidScroll:?
Implementation notes....
Each UIScrollView is part of a custom UIView which uses delegate pattern to call back to the presenting UIViewController subclass that handles coordinating the various contentOffset values.
It is possible to change the content offset of a UIScrollView without triggering the delegate callback scrollViewDidScroll:, by setting the bounds of the UIScrollView with the origin set to the desired content offset.
CGRect scrollBounds = scrollView.bounds;
scrollBounds.origin = desiredContentOffset;
scrollView.bounds = scrollBounds;
Try
id scrollDelegate = scrollView.delegate;
scrollView.delegate = nil;
scrollView.contentOffset = point;
scrollView.delegate = scrollDelegate;
Worked for me.
What about using existing properties of UIScrollView?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
/// The content offset was changed programmatically.
/// Your code goes here.
}
}
Another approach is to add some logic in your scrollViewDidScroll delegate to determine whether or not the change in content offset was triggered programatically or by the user's touch.
Add an 'isManualScroll' boolean variable to your class.
Set its initial value to false.
In scrollViewWillBeginDragging set it to true.
In your scrollViewDidScroll check to see that is it true and only respond if it is.
In scrollViewDidEndDecelerating set it to false.
In scrollViewWillEndDragging add logic to set it to false if the velocity is 0 (as scrollViewDidEndDecelerating won't be called in this case).
Simplifying #Tark's answer, you can position the scrollview without firing scrollViewDidScroll in one line like this:
scrollView.bounds.origin = CGPoint(x:0, y:100); // whatever values you'd like
This is not a direct answer to the question, but if you are getting what appear to be spurious such messages, it can ALSO be because you are changing the bounds. I am using some Apple sample code with a "tilePages" method that removes and adds subview to a scrollview. This infrequently results in additional scrollViewDidScroll: messages called immediately, so you get into a recursion which you for sure didn't expect. In my case I got a nasty impossible to find crash.
What I ended up doing was queuing the call on the main queue:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if(scrollView == yourScrollView) {
// dispatch fixes some recursive call to scrollViewDidScroll in tilePages (related to removeFromSuperView)
// The reason can be found here: http://stackoverflow.com/questions/9418311
dispatch_async(dispatch_get_main_queue(), ^{ [self tilePages]; });
}
}