iOS views not drawing aggressively enough (require force of frame change) - ios

In my app that I've been working on for about 6 months now, I've seen a very weird bug in a few places relating to drawing of views. I've built 20+ ios apps, and this is the only project where I've seen anything like this. I must say it has me scratching my head.
Basically a couple of views do not draw until the frame changes, or for a tableview I have, until I scroll it off screen. These views tend to be outside the bounds of their superview that is eventually resized to reveal them (though not always). When the superview is resized the subview is not redrawn.
This happens even with just a simple UITextView in a nib. However, the text view shows up as soon as I scroll the superview which is a UIScrollView (causing a redraw). The bounds/frames are correct and I've checked all views are not hidden and their alpha is 1.0
Is this expected behaviour? Calling setNeedsDisplay and setNeedsLayout does nothing as this just marks the view as needing to be redrawn.
I put together this absolutely terrible hack to force the drawing, and fortunately I only need to use it in about 2 minor places, but I would really love to know what is going on.
This is probably overkill, but I can't waste anymore time on a bug I believe exists in UIView or CALayer.
//Don't use this unless a view really really really really won't draw when it should
- (void)twForceDraw
{
CGAffineTransform transform = self.transform;
CGRect frame = self.frame;
CGRect bounds = self.bounds;
self.transform = CGAffineTransformIdentity;
self.frame = CGRectZero;
self.bounds = CGRectZero;
self.frame = frame;
self.bounds = bounds;
self.transform = transform;
[self setNeedsLayout];
[self setNeedsDisplay];
}
Any thoughts what might be causing this?

Related

Resizing UIView frame causes content to resize

having some issues getting my head around resizing UIViews and drawing in drawRect. Currently I am performing some custom drawing in a UIView in the drawRect. Now I want to resize the frame but keep the drawing inside the same. Using a UISlider I have:
- (IBAction)changeSize:(id)sender {
//where 20,20 is the original frame position and 72*72 the original size
CGRect newFrame = CGRectMake(20, 20, 72*self.slider2.value, 72*self.slider2.value);
self.square.frame = newFrame;
}
The frame is growing but the contents inside are also getting stretched with it. What am I doing wrong? Any pointers on this would be great. Thanks
self.contentMode = UIViewContentModeRedraw;
This will cause view to redraw itself when bounds change.
UIView contains a boolean property named autoresizesSubviews. So check out the current value and try setting it to false
self.square.autoresizesSubviews = NO;

Resize subviews on animated frame change (autoresizing mask)?

I know that it works if I change the view's frame via the changing of a layer's transform (in this case it simply takes a view with all the subviews and works with it as if it is a single image). It is not good enough for me to use it because:
It is too hard to calculate the appropriate position;
I use HMLauncherView which has some logic based on frames not
transforms.
I tried a lot of ways described here but it seems they are all for autolayout because none of them works. Depending on the type of the autoresizing mask some elements jump to their destination position and some of them are not affected at all or affected via strange ways.
How to solve such issue? Are there ways to solve this via an autoresizing mask?
EDITED
Animation code (the same result is for setting of center/bounds):
[UIView animateWithDuration:0.3 animations:^{
lastLauncherIcon.layer.transform = CATransform3DIdentity;
CGRect r = self.bounds;
r.origin.y = MAX(0, MIN(self.scrollView.contentOffset.y, self.scrollView.contentSize.height - self.bounds.size.height));
launcherIcon.frame = r;
} completion:^(BOOL finished) {
...
}];
The code of layoutSubviews seems to be extra because I have already tried to subclass UILabel which always jumps to the animation destination position. The investigation showed that the frame of this label is set when I set the view's frame/bounds only. In other cases label's setCenter, setFrame and setBounds are called when you call them directly.
This question is very similar but the suggested solution doesn't work for my case. I also found out I had broken something so the view with transformation doesn't appear on iOS 7.1 (maybe 7.x) but works on 8.3.

Animation issues with IOS 8.1

I'm simply trying to move two views upwards in the simplest manner, and I can't figure out how to make it work on iOS8 (but its works just fine on iOS7).
I've read that changes happened but I can't make it work anyway...
Here is the code i'm using :
CGRect txFrame; //the initial frame of my textview
CGRect btFrame; //the initial frame of my button
- (void)kbWillShow{
[UIView animateWithDuration:0.45 animations:^{
//Remembering the initial frames here
txFrame = _txReason.frame;
btFrame = _btSend.frame;
_lbTitleReason.alpha = 0.3;
//Animating
_txReason.frame = CGRectMake(txFrame.origin.x, txFrame.origin.y-55, txFrame.size.width, txFrame.size.height);
_btSend.frame = CGRectMake(btFrame.origin.x, btFrame.origin.y-75, btFrame.size.width, btFrame.size.height);
}completion:nil];
}
- (void)kbWillHide{
[UIView animateWithDuration:0.45 animations:^{
//Putting them back to their original positions.
_txReason.frame = txFrame;
_btSend.frame = btFrame;
_lbTitleReason.alpha = 1;
}completion:nil];
}
I've tried putting the "result" position in the completion block but I just does a very abrupt and weird movement, and after the duraton the view teleports to the intented position. (Which is better than doing an abrupt movement and ending up at the exact same position as it started).
This should be simple, what am I doing wrong?
I have come across this and the answer is to do with the layout constraints. You need to change the constant properties of the layout constraints that affect the view you want to change.
E.g. to change the width of a view with a width NSLayoutConstraint you would call constraint.constant = newWidth; then go ahead with your animation code. This is how I'm doing it and it works fine.
EDIT: your animation code is fine. You don't need to play with the centre property.

UIView not changing position in animateWithDuration

I have two UIViews (My bad it is a UIView and a UIButton) which I am animating at the same time. I originally had a view and a containerView which would animate just fine and worked like a charm.
Now only one of my UIViews will move/animate in animateWithDuration even though through debugging the frame of the other view says that it is in a position it is not.
CGRect rect = self.toggle.frame;
CGRect tabRect = self.tabButton.frame;
rect.origin.x = rect.origin.x - rect.size.width;
NSLog(#"%f Before",tabRect.origin.x);
tabRect.origin.x = tabRect.origin.x - rect.size.width;
NSLog(#"%f After", tabRect.origin.x);
[UIView animateWithDuration:0.3 animations:^{ // animate the following:
self.toggle.frame = rect; // move to new location
self.tabButton.frame = tabRect;
}];
NSLog(#"%f AfterAnimation", tabButton.frame.origin.x);
The toggle view moves fine, but the tabButton view does not animate or move. The strange thing is that both the "After" and "AfterAnimation" debugging code returns the same value, which suggests the frame has indeed moved. Is there a specific reason that this will not work when toggle is a UIView when it would work as a UIContainerView?
Note that if I remove the line
self.toggle.frame = rect;
tabButton will animate correctly, but if I move toggle, tabButton will not move regardless of whether it is first in the animation block or second.
Edit: I have tried moving them into separate blocks and to change the center point rather than the frame, to no avail. It seems that if the toggle view moves, the tabButton will not move.
Edit 2: The pictorial evidence.{
In the following screenshots tabButton bg is green and toggle bg is red.
Above: Initial position (toggle is off-screen) correct position
Above: The problem in question toggle is correct tabButton is not
Above: When self.toggle.frame = rect is commented out (tabButton correct, toggle not)
}
Edit 3: It's even worse than I feared.{
I have done a few more tests and even if I take the toggle change out of the animation block to make it an instant thing, the tabButton will still not animate. This makes me think the tabButton may just fundamentally dislike the toggle view and/or myself so will not move just to spite me.
}
Edit 4:{
If I change the tabButton animation to tabButton.frame = CGRectMake(10,10,100,100) the View snaps instantly to that location and animates back to its original position in the same time as the animation duration.
}
I better add more bookkeeping/TLDR information in case things aren't clear.
toggle is an instance of ToggleDraw which is a subview of UIView which I created.
tabButton is a UIButton which is part of my IB viewController and a property of the class
Both toggle and tabButton are subviews of self.view
The animations will work individually with no modifications to the logic of the rects but will not work if they are animated at the same time
toggle animation seems to take precedence over tabButton animation regardless of the order
I had a problem with the animation of an UIView created in IB (the animation didn't start from the current position of the view, and ended in the initial position).
All worked fine after sending layoutIfNeeded() to the underlaying view before the animation block:
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5) { () -> Void in
...
I think it is not a problem about a UIView Animation. Maybe your toggle posiztion is related to your tabButton. For a try, your can set toggle frame to a rect lick (10, 10, 100,100), then check the result.
I've created an example of what you describe and everything seems to work fine. This is what I used:
UIView *toggle = [[UIView alloc] initWithFrame:CGRectMake(320, 64, 100, 100)];
[toggle setBackgroundColor:[UIColor redColor]];
[self.view addSubview:toggle];
UIButton *tabButton = [[UIButton alloc] initWithFrame:CGRectMake(220, 64, 100, 100)];
[tabButton setBackgroundColor:[UIColor greenColor]];
[self.view addSubview:tabButton];
CGRect rect = toggle.frame;
CGRect tabRect = tabButton.frame;
rect.origin.x = rect.origin.x - rect.size.width;
NSLog(#"%f Before",tabRect.origin.x);
tabRect.origin.x = tabRect.origin.x - rect.size.width;
NSLog(#"%f After", tabRect.origin.x);
[UIView animateWithDuration:1 animations:^{ // animate the following:
toggle.frame = rect; // move to new location
tabButton.frame = tabRect;
}];
What I can suggest is to make sure that the code is being ran on mainthread:
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.3 animations:^{
self.toggle.frame = rect; // move to new location
self.tabButton.frame = tabRect;
}];
});
Also take into account that the log you have after the animation code is incorrect as it won't run after the animation, but rather right next to asking for the animation.
If you want code to run after the animation you should use:
[UIView animateWithDuration:0.3 animations:^{
self.toggle.frame = rect; // move to new location
self.tabButton.frame = tabRect;
} completion:^(BOOL finished){
NSLog(#"Finished animating!");
}];
I have found a solution to the problem. If I initialise the UIButton (tabButton) programmatically rather than through the interface builder, both views will animate simultaneously.
This however seems very hacky to me, kind of like putting a bandaid over a missing foot and hoping it will sort itself out.
I could not work out the root cause of the problem but at least I could avoid the symptoms.
If anyone knows why the views would not animate when the button was made in the interface builder post an answer here, I am interested in knowing the reason behind this.
Thanks for your help everyone.

Animate frame property using CABasicAnimation

I'm trying to make an exact "translation" of this UIView block-based animation code:
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
someView.frame = CGRect(0, 100, 200, 200);
}
completion:nil];
using CABasicAnimation instead.
I'm totally aware that the frame property is actually a combination of position, bounds and anchorPoint of the underlying layer, as it is described here: http://developer.apple.com/library/mac/#qa/qa1620/_index.html
... and I already made a solution like that, using two CABasicAnimations one setting the position, one for bounds and it works for that one view.
The problem is however that I have subviews inside my view. someView has a subview of type UIScrollView in which I place still another subview of type UIImageView. UIScrollView subview has autoresizingMask set to UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight. That all works perfectly if I use the UIView block-based version, however when I try using CABasicAnimations the subviews start behaving unexpectedly(i.e. get resized to incorrect widths). So it seems autoresizingMask is not working correctly when using CABasicAnimations. I noticed also that subviews don't receive a call to setFrame:, although the frame property of the parent view does change after changes to layer position and bounds are made.
That's why I would like to know what would be the correct code to replicate with CABasicAnimation that what is happening when one uses UIView's animateWithDuration method.
I'm totally aware that the frame property is actually a combination of position, bounds and anchorPoint of the underlying layer
Good, but it's important also to be aware that frame is not an animatable property for layers. If you want to animate with CABasicAnimation you must use properties that are animatable for layers. The CALayer documentation marks every such property as explicitly "animatable". The idea of using bounds and position is correct.
Thus, this code does essentially what you were doing before:
[CATransaction setDisableActions:YES];
// set final bounds and position
v.layer.bounds = CGRectMake(0,0,200,200);
v.layer.position = CGPointMake(100,200);
// cause those changes to be animated
CABasicAnimation* a1 = [CABasicAnimation animationWithKeyPath:#"bounds"];
a1.duration = 0.5;
CABasicAnimation* a2 = [CABasicAnimation animationWithKeyPath:#"position"];
a2.duration = 0.5;
[v.layer addAnimation:a1 forKey:nil];
[v.layer addAnimation:a2 forKey:nil];
However, that code has no effect on the size of any sublayers of v.layer. (A subview of v is drawn by a sublayer of v.layer.) That, unfortunately, is the problem you are trying to solve. I believe that by dropping down to the level of layers and direct explicit core animation, you have given up autoresizing, which happens at the view level. Thus you will need to animate the sublayers as well. That is what view animation was doing for you.
This is an unfortunate feature of iOS. Mac OS X has layer constraints (CAConstraint) that do at the layer level what autoresizing does at the view level (and more). But iOS is missing that feature.

Resources