Interaction between UIDynamicAnimator and [UIView animateWithDuration:] - iphone

I'm attempting to animate a view into position in a view that's using UIDynamics. I'm not clear on the interaction between the Core Animation animations and UIDynamics but something's going on that I don't understand.
I have a test project with a view controller with a single view, a label, that starts at the bottom middle of the view and I'm trying to animate motion to the top middle.
// Create the label
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(160, 400, 100, 50)];
label.text = #"hello";
[self.view addSubview:label];
// Set up the animator and collision
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
UICollisionBehavior *collisionBehaviour = [[UICollisionBehavior alloc] initWithItems:#[ label ]];
[collisionBehaviour addBoundaryWithIdentifier:#"wall"
fromPoint:CGPointMake(0, 50)
toPoint:CGPointMake(320, 50)];
[self.animator addBehavior:collisionBehaviour];
// Animate the label
[UIView animateWithDuration:1.0
animations:^{
label.frame = CGRectMake(160, 100, 320, 100);
}];
This is simply a line boundary at the top of the view, nowhere near either the start or end position of my animation. I don't see why it should affect the animation of the button into its initial position, but it does.
If I comment out the addBehaviour: call then the animation works fine. But when I add the behaviour, the animation goes to the wrong location. I don't understand why.

The reason why in your example the label moves left is because you are changing the width of the label from 100px to 320px and the text is right justified.
Now as for why it doesn't move up, when you add the UICollisionBehavior to the label, and then to the UIDynamicAnimator, the UIDynamicAnimator is now in charge of moving the label. When you try and animate it up, the animation goes off, then the UIDynamicAnimator sees the change and sets the center of the label back to what it originally was. You can see this for yourself if you subclass UILabel and NSLog on when the frame and center changes. If you override setCenter to just return your label moves upward.
If you want to move the label either;
Don't attach a UICollisionBehavior to the label and animate it
Also attach a UIDynamicItemBehavior to the label and then addLinearVelocity

You should not set any view's frame manually if that view is managed by a dynamic animator.
The reason why it works without calling -addBehavior: is that the dynamic animator doesn't care about the label until any of its behaviors references the label.
You can move the label manually and start the dynamic animator when the animation is finished, but the better approach is to use a UISnapBehavior to move your label to the desired position.
Also, if you change a view's frame that is managed by a dynamic animator you have to remove all behaviors associated with that view and add them again. You should set your label's initial size and only change it's position from there on, but not its size.

when you set frame of your view below that line write
[self.animator updateItemUsingCurrentState:label];

Related

UIView animation iOS, Bizzarre

I am probably doing something extremely stupid but I cannot figure out why this does not work.
I am trying to perform a simple UIView block animation but have run into trouble. I have recreated in a test project.
I have a view on a View controller, when I press the button, I create a new view and set its frame to be out of the current view (above it). I want to animate the transition so that the view currently on the screen moves downwards out of the view as the new one above it comes down to take its place.
Here is the code which is hooked up to the button,
the original view is hooked up as self.view1
- (IBAction)buttonPressed:(id)sender {
UIView *view2 = [[UIView alloc] init];
view2.backgroundColor = [UIColor blueColor];
float offScreenY = 0 - self.view1.frame.size.height;
CGRect offScreenRect = CGRectMake(0, offScreenY, self.view1.frame.size.width, self.view1.frame.size.height);
view2.frame = offScreenRect;
[self.view addSubview:view2];
float oldY = self.view1.frame.origin.y + self.view1.frame.size.height;
CGRect oldRect = CGRectMake(0, oldY, self.view1.frame.size.width, self.view1.frame.size.height);
[UIView animateWithDuration:0.5 animations:^{
self.view1.frame = oldRect;
view2.frame = CGRectMake(0, 0, self.view1.frame.size.width, self.view1.frame.size.height);
}];
}
This just animates view2 down and does not animate view 1.
If I do not add view 2 as a subview and only put view1's frame change in the animation block then view1 animates correctly.
BUT they will not work together!
Why is this?
This is a classic symptom of having autolayout turned on. If you animate frame, it works, but as soon autolayout reapplies the constraints on the view, view1 will return to its original location. By adding view2, iOS automatically reapplies autolayout constraints immediately and your view1 therefore won't move.
Bottom line, don't use autolayout and try to animate frame properties directly. Two solutions:
The easy solution is to turn off autolayout. Go to IB, select the "File inspector" and uncheck the "Use Autolayout" button:
If you want to keep autolayout on, you shouldn't be animating by changing the frame properties directly. You would animate by change the layout constraint constants. This has been answered elsewhere on S.O., but if you need guidance on that approach, let me know.
The basic idea, though, is to create an IBOutlet for your top constraint for view1 called, say, view1TopConstraint, and then in your animation block you can say
self.view1TopConstraint.constant += self.view1.frame.size.height;
[self.view layoutIfNeeded];
For this to work, though, you'd have to be careful about your other constraints on view1 (e.g., have a height constraint, have no bottom constraint or if you have one, lower its priority, etc.). This can be a hassle the first time you do it, but you'll quickly get the hang of animating by changing constraints.
But, then again, if you're using constraints, you probably shouldn't be defining view2 by its frame, but probably defining constraints for that, too.
In this case it is better for you to have a container view.
Add view1 and view2 inside this container accordingly. Container's some part will be in the screen and container's frame size will be double of the size of a view. Animate the container, so the other two views will be animated accordingly..

Image View in Button Doesn't Appear

I am trying to use the background view of the image view of a UIButton but for some reason it will not show up. This is what I have tried:
detailCell.greenPriorityButton.imageView.frame = detailCell.greenPriorityButton.frame;
[detailCell.greenPriorityButton.imageView setBackgroundColor:[UIColor greenColor]];
[detailCell.greenPriorityButton.imageView setHidden:NO];
[detailCell.greenPriorityButton.imageView setOpaque:YES];
I have called NSLog on the imageView property and it seems everything is as it should be. I can also tap on the button and the method associated with it will be called so I know it exists. Just in case I am missing something here is the NSLog return:
<UIImageView: 0x1d97e1b0; frame = (254 61; 20 20); clipsToBounds = YES; userInteractionEnabled = NO; layer = <CALayer: 0x1d97e210>>
The reason I am not using dot notation to set the image view properties above is because they don't seem to change the values. For example, if I say detailCell.greenPriorityButton.imageView.hidden = NO; it doesn't seem to do anything.
Thanks for any help!
Edit
The reason for not just setting the background color of the button and not its image view is because I am trying to create a small shape in the button. However I want the tappable space to have margins around the shape so it is still user friendly. I thought the image view property would lend useful as I could manipulate the frame and layer of that separately from the frame and layer of the button.
I have now tried adding a UIView *greenBackground as a subview to the button, however this doesn't appear either.
If you want a view that you can have within the view of the button for the purpose of setting its color then I would think that trying to use the imageview is the wrong approach. You could just add a subview that has the changed background color. something like this:
UIView *colorView = [[UIView alloc] initWithFrame:CGRectMake(10, 10, 10, 10)];//Whatever rect you want that will be in reference to the frame of the button
colorView.backgroundColor = [UIColor greenColor];
[detailCell.greenPriorityButton addSubview:colorView];

UISlider won't work when it becomes a subview of a UIView other self.view

When I add my slider as subview of any view besides self.view it does not work (doesn't slide) but it works fine when it is a subview of self.view. You can see it on the other views besides self.view but it doesn't work.
Here is my code:
alphaSlider = [[UISlider alloc] initWithFrame:CGRectMake(0, 0, 90, 10)];
[alphaSlider setMinimumValue:0.01];
[alphaSlider setMaximumValue:1];
[alphaSlider setValue:0.5];
[alphaSlider setUserInteractionEnabled:YES];
[alphaSlider addTarget:self action:#selector(alphaSliderDidChange:) forControlEvents:UIControlEventValueChanged];
alphaSlider.continuous = YES;
[submenu addSubview:alphaSlider];
Any way to fix this?
It may be that submenu is not enabled for user interaction.
[submenu setUserInteractionEnabled:YES];
Another possibility is that the slider's frame is outside of submenu.frame
Since the clipsToBounds property of UIViews is by default NO, the slider would not be clipped despite being outside of submenu's frame. This means that submenu's frame doesn't cover the slider, and there's no way to pass touch events from submenu to submenu's slider. Set the background color of submenu and confirm that the slider is positioned entirely in submenu's frame.
[submenu setBackgroundColor:[UIColor blueColor]];
Submenu's frame might be something like (0, 0, 0, 0).
Include
NSLog(#"%f, %f, %f, %f", submenu.frame.origin.x, submenu.frame.origin.y, submenu.frame.size.width, submenu.frame.size.height);
You could also do
[submenu setClipsToBounds:YES];
and if the slider disappears, then submenu's frame is bad.
Check that the frame height of the slider is not too small to allow interaction. 10 pt is probably too small.
Starting in iOS 8, my sliders also stopped working.
In the past, the frame height of UIViews like UISlider, UISwitch, and UIPickerView has been fixed to some pre-defined value. You could set the frame height, but it would be ignored. Instead of trying to remember, "What is the height of a UISlider in Landscape on the iPad/iPhone?", I had been setting the height to 0 and let the OS assign the default value.
David Braun's answer led me to realize that my UISlider frames had height 0 which disabled user interaction.
I had a similar problem, where the slider did not have the horizontal line, just the white button and did not respond at all.
Based on the above I realized, that my sliders were wider than my safe area. Set some constraints on the 2 sides, and now it works just fine.

How can I make my pattern view extend without stretching?

EDIT - I've worked out what I was originally doing wrong. I was changing the size of the UIScrollView, instead of the pattern subview. I have fixed that, but amended my question with the new problem this has thrown up.
I am making a notes section in my app with a lined-paper effect. The lines are on a separate UIScrollView which responds to scrollViewDidScroll: so the lines and text always move together. My lines are set up like this in viewWillAppear:
CGRect noteLinesRect = CGRectMake(self.view.bounds.origin.x,
self.view.bounds.origin.y,
self.view.bounds.size.width,
noteTextView.contentSize.height+self.view.bounds.size.height);
UIScrollView *anoteXLinesView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.noteXLinesView = anoteXLinesView;
[anoteXLinesView release];
LinePatternView *linesPattern = [[LinePatternView alloc] initWithFrame:noteLinesRect];
self.linesPatternView = linesPattern; [linesPattern release];
[self.noteXLinesView addSubview:self.linesPatternView];
[linesPattern release];
CGPoint newOffset = CGPointMake(self.noteTextView.contentOffset.x, noteTextView.contentOffset.y - NOTE_LINES_OFFSET);
self.noteXLinesView.contentOffset = newOffset;
[self.view insertSubview:self.noteXLinesView atIndex:0];
This works fine when the user first looks at a stored note - all the text is nicely underlined. But when they write more text, eventually they get to the bottom of the lines I created in viewWillAppear and are writing on 'blank paper'. So I need my lined-paper pattern to dynamically get bigger and smaller so it is always a bit bigger than the contentSize of my textView. I am trying to do that like this:
-(void)textViewDidChange:(UITextView *)textView
{
self.linesPatternView.frame = CGRectMake( self.linesPatternView.frame.origin.x, //-self.noteTextView.contentOffset.y+NOTE_LINES_OFFSET,
self.linesPatternView.frame.origin.y,
self.linesPatternView.frame.size.width,
noteTextView.contentSize.height+self.view.bounds.size.height );
}
The problem is, although the lined-paper pattern does increase in size, it doesn't add new lines at the bottom. Instead, the pattern stretches out and gets bigger as the view gets bigger. What am I doing wrong?
One of the solutions is to make 3 views, each containing the lines of the size of your scrollview frame on screen. You position the three one underneath the other in the scrollview and monitor the scrollview through its delegate.
When scrolling down you check:
As soon as the topmost one goes offscreen for more than Y pixels you remove it from the scrollview and insert it underneath the bottom one.
When scrolling up you check:
As soon as the bottommost one goes offscreen for more than Y pixels you remove it from the scrollview and insert it above the top one.
Is there a reason you’re not simply using a tiled pattern as your background view’s backgroundColor? UIColor’s +colorWithPatternImage: will let you set a background that’ll tile indefinitely and won’t stretch as your view resizes. If part of the background has to be different—like the top of your paper, for instance—you can just place an image view containing that at the top of your background view.
Fixed! In addition to the changes outlined in my edit to the question above, I just needed to call [self.linesPatternView setNeedsDisplay]; after the resize.

UIView origin in subview

I've run into an issue that is driving me nuts... I know it must be a simple fix, but I cannot find anything online (but !might be looking in the wrong places).
I have a bunch of buttons that I place in the view with a loop:
//make the button smaller and position
CGRect buttonFrame = button.frame;
buttonFrame.size = CGSizeMake(41, 41);
buttonFrame.origin = CGPointMake(10+(43*i), 415);
button.frame = buttonFrame;
This worked great...but now I want to add them into another view and they disappear. The offending line of code is:
buttonFrame.origin = CGPointMake(10+(43*i), 415);
I believe that this must be working off of the coordinates of self.view and not the new subview that holds the buttons. Do I need a new CGContext for the super view.
I tried something like:
buttonFrame.origin = [newView CGPointMake(10+(43*i), 415)];
But that caused all sorts of errors.
Thanks for your time!
The origin of the CGRect that is assigned as the frame of a view is the offset that the view will be placed at from it's superview.
So if you use a CGRect of (100, 100, 200, 200), the view will be 100 pixels to the right of the origin, and 100 pixels south (also remember that the iOS view coordinate system origin is in the top left corner) and has a size of 200x200.
Your button will be 415 pixels below the local origin (top left corner) of it's superview, which may place it out of view.
buttonFrame.origin = [newView CGPointMake(10+(43*i), 415)]; - this line of code has no sense, because CGPointMake is the C function, and not the ObjectiveC method, so you can't post it like the message to object.
If you want to add subview you have to use [newView addSuview: button]; and make sure you've added your view to another view.