UIImageView rotation + center change cause the image to blow - ios

I am trying to do a UIImageView change every second.
The change can have one of the two:
- Rotation
- Location change
I am using the following:
This is the initialization code:
+(instancetype)newWithFrame:(CGRect)frame {
MySateliteView *v = [[[NSBundle mainBundle] loadNibNamed:#"Satelite" owner:self options:nil] objectAtIndex:0];
v.frame = frame;
v.lastLocation = [SateliteCoordinate new];
return v;
}
This is the full code for the view:
-(void)createSateliteViewFromLocation:(SateliteLocation *)location {
CGPoint center = [self createPointFromLocation:location];
MySateliteView *locationView;
for (MySateliteView *v in _satelites) {
if (v.tag == location.sateliteNumber.integerValue) {
locationView = v;
break;
}
}
if (!locationView) {
locationView = [MySateliteView newWithFrame:CGRectMake(center.x - 15, center.y - 15, 30, 30)];
locationView rotate:location.coordinates.degree];
locationView.tag = location.sateliteNumber.integerValue;
[_satelites addObject:locationView];
[UIView animateWithDuration:0.7 animations:^{
[self addSubview:locationView];
}];
} else {
if ([locationView needsNewCenter:location.coordinates]) {
[UIView animateWithDuration:0.2 animations:^{
locationView.bounds = CGRectMake(0, 0, 30, 30);
locationView.center = center;
locationView.superview.clipsToBounds = YES;
}];
}
if ([locationView needsRotate:location.coordinates]) {
[locationView rotate:location.coordinates.degree];
}
}
}
For some reason, once the center is changed with a rotation already applied, the image is getting larger!
Anyone have any idea why and what I can do?
Thanx!

The frame is getting larger because you have constraints in a view and you're messing around with the center manually. You shouldn't mix auto layout and manual layout in this way. I'd recommend either:
Remove the constraints and do all your view layout manually by overriding layoutSubviews and drawRect: as appropriate while continuing to keep the code you already have.
Interact with the constraints instead of the frame/center/bounds. You can create an IBOutlet from the constraints in the xib so you can modify their values. You can override the system provided updateConstraints method in your view to ensure constraints are provided appropriately.
An example of updating the constraints to move your view might look like this:
[UIView animateWithDuration: .7, animations: ^{
self.constraint1.constant = 23;
[NSLayoutConstraint deactivateConstraints: #[self.constraint2]];
[self layoutIfNeeded];
}];

Related

iOS Layer animation with auto layout

I have a simple container view(green) and two sub views(red and blue) as below.
The container view is not applying auto layout and I config its size & location by frame.
While the sub views are applying auto layout(see code below)
#implementation XXView {
UIView *_leftView;
UIView *_rightView;
}
- (instancetype)init {
self = [super initWithFrame:CGRectZero];
if (self) {
[self setupViewHierarchy];
[self setupConstraints];
}
return self;
}
- (void)setupViewHierarchy {
_leftView = [[UIView alloc] init];
_leftView.backgroundColor = UIColor.redColor;
_leftView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_leftView];
_rightView = [[UIView alloc] init];
_rightView.backgroundColor = UIColor.blueColor;
_rightView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_rightView];
self.backgroundColor = UIColor.greenColor;
}
- (void)setupConstraints {
[NSLayoutConstraint activateConstraints:#[
[_leftView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:10],
[_leftView.topAnchor constraintEqualToAnchor:self.topAnchor],
[_leftView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[_leftView.widthAnchor constraintEqualToConstant:50],
[_rightView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-10],
[_rightView.topAnchor constraintEqualToAnchor:_leftView.topAnchor],
[_rightView.bottomAnchor constraintEqualToAnchor:_leftView.bottomAnchor],
[_rightView.widthAnchor constraintEqualToAnchor:_leftView.widthAnchor],
]];
}
...
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
XXView *tv = [[XXView alloc] init];
CGFloat width = self.view.bounds.size.width;
tv.frame = CGRectMake(50, 400, width-100, 100);
[self.view addSubview:tv];
self.tv = tv;
}
Then I would like to animate the container's width change by using the CABasicAnimation as below:
- (void)startAnimation {
[CATransaction begin];
CGFloat width = self.bounds.size.width;
CABasicAnimation *widthAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size.width"];
widthAnimation.fromValue = #(width/2);
widthAnimation.toValue = #(width);
widthAnimation.duration = 1;
[self.layer addAnimation:widthAnimation forKey:#"123"];
[CATransaction commit];
}
However, the animation is not as I would expect. I would expect the left view moves as the container's leading side and the right view does as the trailing side.
What I see is, the green view expands as expected and the left view moves as green view's leading side. However, the right view is always keeping the same distance to left view. Below is the screenshot taken at the beginning of the animation.
Why the animation is not working as expected?
The problem is that your CABasicAnimation is modifying the bounds of the layer ... but as far as auto-layout is concerned that does not change the width of the view.
It's not really clear what your goal is here, but if you change your animation method to this it might get you on your way:
- (void)startAnimation {
CGRect f = self.frame;
CGFloat fw = f.size.width;
f.size.width = fw / 2;
self.frame = f;
[self layoutIfNeeded];
f.size.width = fw;
[UIView animateWithDuration:1.0 animations:^{
self.frame = f;
[self layoutIfNeeded];
}];
}

Change height of inputAccessoryView issue

When I change the height of inputAccessoryView in iOS 8, the inputAccessoryView not go to the right origin, but covers the keyboard.
Here are some code snippets:
in table view controller
- (UIView *)inputAccessoryView {
if (!_commentInputView) {
_commentInputView = [[CommentInputView alloc] initWithFrame:CGRectMake(0, 0, [self width], 41)];
[_commentInputView setPlaceholder:NSLocalizedString(#"Comment", nil) andButtonTitle:NSLocalizedString(#"Send", nil)];
[_commentInputView setBackgroundColor:[UIColor whiteColor]];
_commentInputView.hidden = YES;
_commentInputView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin;
}
return _commentInputView;
}
in CommentInputView
#when the textview change height
- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
if (height > _textView_height) {
[self setHeight:(CGRectGetHeight(self.frame) + height - _textView_height)];
[self reloadInputViews];
}
}
in UIView Category from ios-helpers
- (void)setHeight: (CGFloat)heigth {
CGRect frame = self.frame;
frame.size.height = heigth;
self.frame = frame;
}
Finally, i found the answer. In ios8, apple add a NSContentSizeLayoutConstraints to inputAccessoryView and set a constant with 44. You can't remove this constaint, because ios8 use it to calculate the height of inputAccessoryView. So, the only solution is to change value of this constant.
Example
in ViewDidAppear
- (void)viewDidAppear:(BOOL)animated {
if ([self.inputAccessoryView constraints].count > 0) {
NSLayoutConstraint *constraint = [[self.inputAccessoryView constraints] objectAtIndex:0];
constraint.constant = CommentInputViewBeginHeight;
}
}
change inputAccessoryView height when the textview height changed
- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
NSLayoutConstraint *constraint = [[self constraints] objectAtIndex:0];
float new_height = height + _textView_vertical_gap*2;
[UIView animateWithDuration:0.2 animations:^{
constraint.constant = new_height;
} completion:^(BOOL finished) {
[self setHeight:new_height];
[self reloadInputViews];
}];
}
That is.
One way you can update the constraint mentioned in Yijun's answer when changing the height of the inputAccessoryView is by overwriting setFrame: on your inputAccessoryView. This doesn't rely on the height constraint being the first in the array.
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
for (NSLayoutConstraint *constraint in self.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.constant = frame.size.height;
break;
}
}
}
The first answer didn't totally solve my problem but gave me a huge hint.
Apple did add a private constraint to the accessory view, but you cannot find it in the constraint list of the accessory view. You have to search for it from its superview. It killed my a few hours.
After reading the answer above, which is a great find, I was concerned that relying on the constraint you need to change being [0] or firstObject is an implementation detail that's likely to change under us in the future.
After doing a bit of debugging, I found that the Apple-added constraints on the accessory input view seem to have a priority of 76. This is a crazy low value and not one of the listed enums in the documentation for priority.
Given this low priority value it seems like a cleaner solution to simply conditionally add/remove another constraint with a high priority level, say UILayoutPriorityDefaultHigh when you want to resize the view?
For Xcode 11.2 and swift 5 this function will update inputAccessoryView constraints even in animation block
func updateInputContainerConstraints() {
if let accessoryView = inputAccessoryView,
let constraint = accessoryView.superview?.constraints.first(where: { $0.identifier == "accessoryHeight" }) {
constraint.isActive = false
accessoryView.layoutIfNeeded()
constraint.constant = accessoryView.bounds.height
constraint.isActive = true
accessoryView.superview?.addConstraint(constraint)
accessoryView.superview?.superview?.layoutIfNeeded()
}
}
Try this:
_vwForSendChat is the input accessory view
_txtViewChatMessage is the textview inside input accessory view
-(void)textViewDidChange:(UITextView *)textView {
CGFloat fixedWidth = textView.frame.size.width;
CGSize newSize = [textView sizeThatFits:CGSizeMake(fixedWidth, MAXFLOAT)];
CGRect newFrame = textView.frame;
newFrame.size = CGSizeMake(fmaxf(newSize.width, fixedWidth), newSize.height);
if (newFrame.size.height < 40) {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, 40);
} else {
if (newFrame.size.height > 200) {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, 200);
} else {
_vwForSendChat.frame = CGRectMake(0, 0, self.view.frame.size.width, newFrame.size.height);
}
}
[self.txtViewChatMessage reloadInputViews];
}

Calling layoutIfNeeded inside UIView animation block cause subviews to move earlier

I'm trying to create a custom callout bubble.
I have an animation block setting the scale transform of the bubble UIView:
self.view.transform = CGAffineTransformMakeScale(0, 0); // (1)
self.view.transform = CGAffineTransformIdentity; // (2)
I either start the bubble view with scale (0, 0) as in (1) and animate to Identity as in (2) inside the animation block, or the opposite, going from (2) to (1).
If I don't have the [self.view layoutIfNeeded]; in my animation block, going from (1) to (2) works fine as in:
But when going back from (2) to (1) without the [self.view layoutIfNeeded];, the subviews jump to the left before the animation is finished:
Now, if I do add the [self.view layoutIfNeeded]; the subviews animate with the bubble view, but with a sort of delay:
Either going from (1) to (2):
Or from (2) to (1):
I have already tried replacing all the top and leading subview's constraints with center constraints like in How do I adjust the anchor point of a CALayer, when Auto Layout is being used? and have also tried the layer transform solution (but this one breaks the layout constraints saying it can't satisfy all constraints).
Any ideas on how to solve my animation problem?
Thanks in advance.
UPDATE:
I'm updating the question with the actual method that I call on
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)annotationView
and
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)annotationView
to present and dismiss my custom callout bubble (forget the Expanded state for now).
- (void)setCalloutState:(CalloutState)calloutState
animated:(BOOL)animated
annotationView:(MKAnnotationView *)annotationView
completion:(void (^)(BOOL finished))completion
{
if (self.view.superview == annotationView || !annotationView) {
} else {
// [self.view.layer removeAllAnimations];
[self.view removeFromSuperview];
self.view.transform = CGAffineTransformIdentity;
self.view.bounds = CGRectMake(0, 0, normalViewWidth, normalViewHeight);
self.view.transform = CGAffineTransformMakeScale(0, 0);
self.view.center = CGPointMake(annotationView.bounds.size.width / 2, 0);
[annotationView addSubview:self.view];
}
void (^animationBlock)(void) = ^{
self.view.transform = CGAffineTransformIdentity;
switch (calloutState) {
case CalloutStateHidden:
self.view.bounds = CGRectMake(0, 0, normalViewWidth, normalViewHeight);
self.view.transform = CGAffineTransformMakeScale(0, 0);
break;
case CalloutStateNormal:
self.view.bounds = CGRectMake(0, 0, normalViewWidth, normalViewHeight);
break;
case CalloutStateExpanded:
self.view.bounds = CGRectMake(0, 0, expandedViewWidth, expandedViewHeight);
break;
default:
break;
}
self.view.center = CGPointMake(annotationView.bounds.size.width / 2, 0);
[self.view layoutIfNeeded];
};
if (animated) {
// TODO: figure out why the first animateWithDuration is needed in this nested thing
[UIView animateWithDuration:0
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:NULL
completion:^(BOOL finished) {
[UIView animateWithDuration:1.3
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:animationBlock
completion:^(BOOL finished) {
if (finished) {
self.calloutState = calloutState;
// TODO: figure out how to end UIView animation instantly so we don't need the second condition at the if
// having a concurrency problem here
if (calloutState == CalloutStateHidden && self.view.superview == annotationView) {
[self.view removeFromSuperview];
}
self.editingEnabled = calloutState == CalloutStateExpanded;
}
if (completion) {
completion(finished);
}
}];
}];
// ---------------------------------------------------------------------------------
} else {
animationBlock();
self.calloutState = calloutState;
if (calloutState == CalloutStateHidden) {
[self.view removeFromSuperview];
}
self.editingEnabled = calloutState == CalloutStateExpanded;
if (completion) {
completion(YES);
}
}
}
This might help. As per Apple documentation -layoutIfNeeded forces layout early
- (void)setNeedsLayout;
- (void)layoutIfNeeded;
So, you can try setNeedsLayout. This should satisfy the first case you mentioned.

Resizing UIView with animation, subview won't animate

Using UIView's animateWithDuration:animations:completion:, I'm resizing a UIView and a subview of that UIView which is a subclass of UITableView.
The UIView resizes fine, but the UITableView doesn't. It does move around a little, but the frame does not update properly and reverts to its original state.
Edit: if I move the resizing to the completion block.... it works. What gives?
tweetTable.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[tweetTable endUpdates];
[UIView animateWithDuration:DURATION animations:^{
CGRect leftFrame = leftPane.frame;
leftFrame.size.width = self.view.frame.size.width - MARGIN;
leftPane.frame = leftFrame;
leftPaneButton.frame = leftFrame;
CGRect tweetFrame = tweetTable.frame;
tweetFrame.size.width = leftPane.frame.size.width;
NSLog(#"%f to %f", tweetTable.frame.size.width, leftPane.frame.size.width);
tweetTable.frame = tweetFrame;
tweetTable.backgroundColor = [UIColor redColor];
tweetTable.alpha = 0.5f;
sideInfo.center = CGPointMake(self.view.frame.size.width - MARGIN + (sideInfo.frame.size.width / 2), sideInfo.center.y);
rightPaneButton.center = sideInfo.center;
} completion:^(BOOL finished) {
leftExtended = TRUE;
[tweetTable beginUpdates];
}];
Check in your storyboard if the UIView has Autoresize Subviews checked. That means that it resizes all of its subviews when the view itself gets resized. That would explain why it works in the completion block.

UIView animateWithDuration doesn't work with Rect width/height animation

The following code works. It animates all those components nicely
CGRect logoRect = self.logoImageView.frame;
CGRect loginBackgroundRect = self.loginControlsBkImageView.frame;
CGRect loginButtonRect = self.loginButton.frame;
CGRect tableViewRect = self.tableView.frame;
CGRect forgotPasswordRect = self.forgotButton.frame;
CGRect signupButtonRect = self.signUpButton.frame;
if (!iPhone) {
// ipad keyboard-on-screen re-layout
logoRect.origin.y-= 60;
loginBackgroundRect.origin.y-= 110;
loginButtonRect.origin.y-=110;
tableViewRect.origin.y-=110;
forgotPasswordRect.origin.y-=110;
signupButtonRect.origin.y-=200;
}
else {
// iphone keyboard-on-screen re-layout
if (portrait) {
logoRect.origin.y-=17;
logoRect.origin.x-=50;
loginBackgroundRect.origin.y-= 70;
loginButtonRect.origin.y-=70;
tableViewRect.origin.y-=70;
forgotPasswordRect.origin.y-=70;
//signupButtonRect.origin.y+=200; // get off screen!
} else {
logoRect.origin.y-= 30;
loginBackgroundRect.origin.y-= 25;
loginButtonRect.origin.y-=25;
tableViewRect.origin.y-=25;
}
}
[UIView animateWithDuration:0.2f
delay:0.0f
options:UIViewAnimationOptionCurveEaseIn
animations:^(void) {
self.logoImageView.frame = logoRect;
self.loginControlsBkImageView.frame = loginBackgroundRect;
self.loginButton.frame = loginButtonRect;
self.tableView.frame = tableViewRect;
self.forgotButton.frame = forgotPasswordRect;
self.signUpButton.frame = signupButtonRect;
}
completion:NULL];
Take the following code and add one line (see below) to animate the WIDTH of the logoImageView... and puff... only the logoImageView animation works - the rest simply doesn't move. as if the frame size animation causes everything else not to animate if in the same animation block.
if (portrait) {
logoRect.origin.y-=17;
logoRect.origin.x-=50;
loginBackgroundRect.origin.y-= 70;
loginButtonRect.origin.y-=70;
tableViewRect.origin.y-=70;
forgotPasswordRect.origin.y-=70;
//signupButtonRect.origin.y+=200; // get off screen!
} else {
logoRect.origin.y-= 30;
logoRect.size.width-= 30; // <------- LINE BEING ADDED HERE
loginBackgroundRect.origin.y-= 25;
loginButtonRect.origin.y-=25;
tableViewRect.origin.y-=25;
}
I'm at a loss here. Does anyone know what's going on?
Try to change whole frame, not just width. It should work.
Put these lines before commiting animation (not in block):
self.logoImageView.frame = logoRect;
etc.
And instead of using animate method try to use commit animations this way:
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.75];
// Here some more animation settings
// Here your animations
[UIView commitAnimations];

Resources