Animating UIView based on as-yet undetermined layout - ios

I have a custom view, CustomView, that can animate itself in response to an animate message:
// in a view controller
[self.customView animate];
Inside -[CustomView animate], the logic is as follows:
- (void)animate {
for each subview {
startFrame = subview.frame;
endFrame = EndFrameFromStartFrame(startFrame);
subview.animation = AnimationLoopBetween(startFrame, endFrame);
}
}
* This block is pseudocode.
That is to say, the animate method examines the current frame of each subview and generates an animation ending frame for each subview which is essentially a frame which is a fixed ratio larger than the current frame. Then, it creates one animation per subview that varies the frame of its subview, alternating between the current (or start) frame and the end frame, calculated earlier.
This method works very well in general. It allows CustomView to animate properly regardless of how its superview positions and sizes it.
However, in a few cases, "users" of CustomView (that is, view controllers) call animate very early in their lifetime – so early, in fact, that Auto Layout has not yet made any constraint or layout passes. This means that animate is called when all of CustomView's subview frames are CGRectZero. As you can probably guess, this causes both the starting and ending values of the animations to be calculated incorrectly.
Even after the layout pass is complete, and the views' model layers have the proper frames, the CustomView instance is still not visible, because its subviews are happily animating from one zero-rect to another. Not good.
How should I restructure this animation behavior to allow users of this class to call animate at any time without negative repercussions?
Here are some possible answers I've considered, and why they don't seem satisfactory to me:
In any view controller that uses CustomView, put off the call to animate until viewDidLayoutSubviews.
This forces an unwieldy restriction on view controllers that use this view.
In CustomView's layoutSubviews method, first call [super layoutSubviews], then restart the animation if it was already running.
An interesting idea, but CustomView has more than one level of subviews. Thus, even after [super layoutSubviews], many of the views further down the view hierarchy will not yet have valid frames, meaning animate will still misbehave.
In CustomView's layoutSubviews method, first call [super layoutSubviews], then manually layout any required views' frames using [<view> layoutIfNeeded], before finally restarting the animation if it was already running.
This might actually work, but doesn't seem like a good idea. As much as possible, I'd like to leave layout work to Auto Layout.

Why not just skip the uninteresting case?
- (void)animate {
if (CGRectEqualToRect(self.frame, CGRectZero) return;
// ...animate.
}

Related

Why viewDidLayoutSubviews is called twice only on initialize?

I am going to use it by adding a child VC. (Coding with code)
ChildVC is using autolayout (snapkit).
At initialization, viewDidLayoutSubviews is called twice.
At the first call, the childVC.View is set to full screen Frame.
In the second call, it is set according to the autolayout setting.
I found out that if the child VC's Frame is not set, it is set to full screen.
(Resize View From NIB)
I want to know how to turn off Resize View From NIB programmatically.
The problem is, I want to know why viewDidLayoutSubviews is called twice.
I'm curious as to why the childVC.View size changes when the second call is made.
I want to know one more thing. Isn't this usually used when setting subView's witdh and Height?
someView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
make.width.equalTo(view.bounds.width)
make.height.equalTo(view.bounds.height * 0.58)
}
Based on VC.View bounds ratio?

Issue while using viewDidLayoutSubviews

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.

What method is called on UIView when it's resized by autolayout?

I have an image view that I'm implementing rounded corners on by overriding the following in my subclass:
-(void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self.layout setCornerRadius:frame.size.width/10.0];
}
This works great for the initial display of the image, but if the size of the image changes due to device rotation or some other mechanism, this method does not get called to implement the resize.
I'm using autolayout, and I want to know what method of UIView (and thus UIImageView) is being called when my constraints resize the view so that I can recalculate my corner radius whenever this resize takes place. My (apparently false) assumption was that the autolayout system called setFrame: to move / resize views as needed.
From the docs on updateConstraints:
Custom views that set up constraints themselves should do so by overriding this method
....
Before layout is performed, your implementation of updateConstraints will be invoked, allowing you to verify that all necessary constraints for your content are in place at a time when your custom view’s properties are not changing.
Or from the docs on layoutSubviews:
You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly.
but if you need to do this on rotational changes, check out willTransitionToTraitCollection:withTransitionCoordinator:
Implementors of this method can use it to adapt the interface based on the values in the newCollection parameter. A common use of this method is to make changes to the high-level presentation style when the current size class changes.

Complex layout in UIView that animates its content

I'm having this general question about a problem that arises very often when I'm designing complex UIViews that require a special layout and animations at the same time.
What I'm currently building is the same kind of view that the Mail app uses on iOS to present a list of recipients when writing an email (the blue badges container) :
So I understand and know how I would build such a view, but I always have this question when facing such a case : How do you handle the complex layout (layouting all the blue rounded rectangles) and their animations at the same time (when adding or removing a recipient) ?
1. Normally I would reimplement :
- (void)layoutSubviews
to reflect the state of the view at the layout moment (ie layout each blue rounded UIButton side by side) according to my current bounds and just add and animate a UIButton when someone adds a new person to the list.
But what would happen to the animation if a layout pass is already running? (this may be a dumb question since everything is supposed to happen on the main thread and so no concurrency is involved here, but I'm not sure on how to handle this)
I think those two things (layout and animations) are not supposed to happen at the same time, since the main runloop "enqueues work and dequeues work" one "block" at a time (but maybe an animation is just hundreds of blocks enqueued that draw different things overtime, which could easily be compromised by a layout block in between??)
2. Also, would this solution be acceptable ?
Reimplement layoutSubviews the exact same way to handle the correct layout of my subviews
When someone adds or deletes a person, just call this to re-position everything animated
[UIView animateWithDuration:0.25f animations:^{
[self setNeedsLayout];
[self layoutIfNeeded];
}];
As you can see, lots of questions here, I don't know exactly how to handle this gracefully.
Both of these solutions have been tested and approved but the thing is which one is the best ?
Do you have a better solution ?
Thanks for your help!
As I understood, you mean animations applied to a view for which layoutSubviews method is implemented, e.g. animated change of its frame. If something is changed in your view while animation is ongoing that causes the view to re-layout, then layoutSubviews method will be called.
Basically, such kind of animations is nothing more than following:
change view's frame (actually, frame of a view's layer) by some small value (e.g. increase origin's Y coordinate by 1 pixel)
call layoutSubviews, if needed
redraw view (i.e. its layer)
change view's frame by some small value
call layoutSubviews, if needed
redraw view
...
repeat steps above until view's properties reach target values
All those steps are performed on the main thread, so steps above are consecutive and are not concurrent.
Generally speaking, the layoutSubviews method should define coordinates and sizes of subviews basing on its own bounds. Animations change the view's frame and correspondingly its bounds, so your implementation of the layoutSubviews method is supposed to handle those changes correctly.
That is, the answer for your question is implement correctly layoutSubviews method.
P.S. This answer has a great explanation of how animations are implemented.
Update
It seems that previously I understood the question incorrectly. My understanding was:
What happens if layoutSubviews method is called for a view being animated?
That is, I assumed that there is some view which e.g. changes its y coordinate with animation, the layoutSubviews method is called for this view during animation, and some subview of the view changes its position in the layoutSubviews.
After clarification from #Nicolas, my understanding is as follows:
Let's have some view ('Parent') with a subview ('Child'); let's animate this subview ('Child'); let's call layoutSubviews method of the 'Parent' view and change 'Child' subview's frame in this layoutSubviews method while animation is ongoing. What will happen?
Background Theory:
UIView itself doesn't render any content; it's done by Core Animation layers. Each view has an associated CALayer which holds a bitmap snapshot of a view. In fact, UIView is just a thin component above Core Animation Layer which performs actual drawing using view's bitmap snapshots. Such mechanism optimizes drawing: rasterized bitmap can be rendered quickly by graphics hardware; bitmap snapshots are unchanged if a view structure is not changed, that is they are 'cahced'.
Core Animation Layers hierarchy matches UIView's hierarchy; that is, if some UIView has a subview, then a Core Animation layer corresponding to the container UIView has a sublayer corresponding to the subview.
Well... In fact, each UIView has even more than 1 corresponding CALayer. UIView hierarchy produces 3 matching Core Animation Layers trees:
Layer Tree - these are layers we used to use through the UIView's layer property
Presentation Tree - layers containing the in-flight values for any running animations. Whereas the layer tree objects contain the target values for an animation, the objects in the presentation tree reflect the current values as they appear onscreen. You should never modify the objects in this tree. Instead, you use these objects to read current animation values, perhaps to create a new animation starting at those values.
Objects in the render tree perform the actual animations and are private to Core Animation.
Change of UIView's properties such as frame is actually change of CALayer's property. That is, UIView's property is a wrapper around corresponding CALayer property.
Animation of UIView's frame is actually change of CALayer's frame; frame of the layer from Layer Tree is set to the target value immediately whereas change of frame value of layer from presentation tree is stretched in time. The following call:
[UIView animateWithDuration:5 animations:^{
CGRect frame = self.label.frame;
frame.origin.y = 527;
self.label.frame = frame;
}];
doesn't mean that self.label's drawRect: method will be called multiple times during next 5 seconds; it means that y-coordinate of the presentation tree's CALayer corresponding to the self.label will change incrementally from initial to target value during these 5 seconds, and self.label's bitmap snapshot stored in this CALayer will be redrawn multiple times according to changes of its y-coordinate.
Answer:
Given this background, now we can answer the original question.
So, we have ongoing animation for a child view, and layoutSubviews method gets called for a parent view; in this method, child view's frame gets changed. It means that frame of a layer assiciated with the child view will be immediately set to the new value. At the same time, layer from the presentation tree has some intermidiate values (according to ongoing animation); setting new frame just changes target value for presentation tree layer, so that animation will continue to the new target.
That is, result of situation described in the original question is a 'jumping' animation. Please see demonstration in the GitHub sample project.
Solution for such complex cases
In your layoutSubviews method, if an animation is ongoing, you need to use in-flight animation coordinates. You can obtain them with the presentationLayer method of CALayer associated with a view. That is, if a view being animated is called aView, then presentation layer for this view can be accessed using [aView.layer presentationLayer].

What conditions can prevent layoutSubviews from being called after setNeedsLayout?

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.

Resources