Recently I used UIDynamics to animate an image view into place. However, because its autolayout y-pos constraint was set to off-screen, when navigating away from the screen and then returning to it, my image view was being placed off-screen again. The animation took about 3 seconds, so after three seconds I just reset the constraint. That feels a little hacky.
So my question is this: what is the proper way to handle autolayout and UIDynamics at the same time?
This is not really a dynamics problem. Autolayout is incompatible with any view animation, or any manual setting of the frame: when layout comes along, it is the constraints that will be obeyed. It is up to you, if you move a view manually in any way, to update the constraints to match its new position/size/whatever.
Having said that: with UIKit Dynamics, when the animation ends, the animator will pause, and the animator's delegate is notified:
https://developer.apple.com/library/ios/documentation/uikit/reference/UIDynamicAnimatorDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UIDynamicAnimatorDelegate/dynamicAnimatorDidPause:
So that is the moment to update the constraints.
You have a nice solution provided by Geppy Parziale in this tutorial.
Basically you can create an object that conforms to UIDynamicItem:
#interface DynamicHub : NSObject <UIDynamicItem>
#property(nonatomic, readonly) CGRect bounds;
#property(nonatomic, readwrite) CGPoint center;
#property(nonatomic, readwrite) CGAffineTransform transform;
#end
That needs to init the bounds or it will crash:
- (id)init {
self = [super init];
if (self) {
_bounds = CGRectMake(0, 0, 100, 100);
}
return self;
}
And then you use UIDynamics on that object and use the intermediate values to update your constraints:
DynamicHub *dynamicHub = [[DynamicHub alloc] init];
UISnapBehavior *snapBehavior = [[UISnapBehavior alloc] initWithItem:dynamicHub
snapToPoint:CGPointMake(50.0, 150.0)];
[snapBehavior setDamping:.1];
snapBehavior.action = ^{
self.firstConstraint.constant = [dynamicHub center].y;
self.secondConstraint.constant = [dynamicHub center].x;
};
[self.animator addBehavior:snapBehavior];
Related
I am aware that this question has been asked many times, but I have found a partial resolution.
I have a custom class GraphView in which I have several sliders which change the graph parameters and instigate a redraw using [self setNeedsDisplay]. The only way I can get the setNeedsDisplay to work is to have the view of type GraphView just under the View Controller and the slider just under (and inside) the GraphView (in the storyboard hierarchy). This is problematic since the slider must be inside the graph.
Here is an mcve #interface:
#import <UIKit/UIKit.h>
#interface GraphView : UIView
#property float red;
#property __weak IBOutlet UITextField *redout;
#property UIBezierPath *aPath;
#property CGPoint aPoint;
- (void)drawRect:(CGRect)rect;
- (id) initWithFrame:(CGRect)frameRect;
- (IBAction)red_rabi:(id)sender;
Here is the mcve #implementation:
#import "GraphView.h"
#implementation GraphView
- (id) initWithFrame:(CGRect)frameRect
{
if ((self = [super initWithFrame:frameRect]) != nil)
{
_red=1.0;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
int i;
NSString *redstr;
float width, height;
width = rect.size.width;
height = rect.size.height;
_aPath =[UIBezierPath bezierPathWithRect:rect] ;
[_aPath setLineWidth:1.0];
_aPoint.x=0.0;
_aPoint.y=0.0;
[_aPath moveToPoint:_aPoint];
redstr = [NSString localizedStringWithFormat:#"%6.2f", _red];
for (i=1;i<400;i++)
{
_aPoint.x=i*width/400.0;
_aPoint.y=height-height*(sin(i*_red/30.)+1.0)/2.0;
[_aPath addLineToPoint:_aPoint];
}
[_aPath stroke];
}
- (IBAction)red_rabi:(id)sender
{
NSString *redstr;
UISlider *slider = (UISlider *)sender;
_red= slider.value;
redstr = [NSString localizedStringWithFormat:#"%6.2f", _red];
_redout.text = redstr;
[self setNeedsDisplay];
}
#end
If you place a generic View just underneath the View Controller (which I didn't touch), change the generic View's class to GraphView, and place a slider and TextField inside the GraphView (connecting them to the outlet and action), this app will generate a few cycles of a sine wave with the frequency controlled by the slider and its value displayed in the TextField.
If you want the slider and TextField in another view, one must use an enveloping view for all three items (GraphView, slider, text) and one cannot connect the slider and TextField to the GraphView using Ctrl_drag to the GraphView.h file. To remedy this, I placed a generic Object at the highest level and renamed it GraphView - I could then connect the slider and TextField. Although the Textfield reads correctly, the slider doesn't update the GraphView.
By the way, essentially the same code with the GraphView and slider in separate views works perfectly in OS X.
Sorry for the length of this query and thanks!
Problem solved! Due to an interesting SO post from three years ago (about connecting to subviews of UIView), I discovered that one merely drags (not Ctrl_drag!) from the action or outlet circle (in the .h file) to the control and that's it. Works perfectly even when the controls are in a different view from the subclassed UIView. Works equally well with outlets as with actions though you always drag away from the circle.
Thanks to all for their help.
I am trying to implement UIDynamicAnimator effects on a scrollView with many elements. I couldn't find too much information about it anywhere ,so i just need some starting point.
So i have my scrollView, with many UIViews in it. i want to make any kind of animation to its "subViews" while scrolling.
Tried that-nothing happens .
//DYNAMICS ANIMATIONS
UIDynamicAnimator *dynamic=[[UIDynamicAnimator alloc] initWithReferenceView:scroller];
UIGravityBehavior *gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:mainCellsArray];
[dynamic addBehavior:gravityBeahvior];
When mainCellsArray holds all the "subViews" of the scroller .
EDIT:
I have a strong property for the dynamic .
The array holds my costume classes pointers , that each class is a UIView subclass, and they all the scroller children's.
First, make sure you define your animator as a property, not solely as a local variable (and I tend to use animator for the name of that, to avoid confusion with the #dynamic keyword):
#property (strong, nonatomic) UIDynamicAnimator *animator;
Then instantiate the animator:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.scrollView];
And add the gravity:
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:#[viewToAnimate]];
[self.animator addBehavior:gravityBehavior];
If you want them to stop when they hit the bottom of the contentSize of the scroll view, you can't use the typical translatesReferenceBoundsIntoBoundary setting. You have to make a path yourself, e.g. something like:
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:#[viewToAnimate]];
CGRect contentSizeRect = {CGPointZero, self.scrollView.contentSize};
UIBezierPath *path = [UIBezierPath bezierPathWithRect:contentSizeRect];
[collision addBoundaryWithIdentifier:#"contentSize" forPath:path];
[self.animator addBehavior:collision];
Or, if you want them to fly off the scroll view, you probably want to remove them when they no longer intersect the contentSize of the scroll view:
UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:#[viewToAnimate]];
UIGravityBehavior __weak *weakGravity = gravity;
CGRect contentSizeRect = {CGPointZero, self.scrollView.contentSize};
gravity.action = ^{
if (!CGRectIntersectsRect(contentSizeRect, viewToAnimate.frame)) {
NSLog(#"removing view");
[viewToAnimate removeFromSuperview];
[self.animator removeBehavior:weakGravity];
}
};
[self.animator addBehavior:gravity];
so there must be something simple I am missing because I just can't figure out how to move my UIView
This is what I have so far
my .m file
- (void)viewDidLoad
{
[super viewDidLoad];
[self buttonLayout];
}
-(void)buttonLayout
{
UIView *theView = [self buttonGroup];
theView.center=CGPointMake(50, 50);
}
My .h file
#property (nonatomic, retain) IBOutlet UIView *buttonGroup;
So this is what I have so far and i just cant seem to get it to move at all
P.S i don't want it animated as i'm getting my layout to move to compensate for different iPhone screen sizes :)
I have not tested this, but it seems like you need to:
A) Remove the view from its parent.
B) Adjust the center of its 'frame', not the view.
C) Re-add the subview.
Like so:
UIView *theView = [self buttonGroup];
CGRect theFrame = theView.frame;
[theView removeFromSuperview];
theFrame.origin.x = 50.0;
theFrame.origin.y = 50.0;
theView.frame = theFrame;
[self.view addSubview:theView];
This should work with autolayout.
As the other poster says, you can't move views around by changing their center or frame when you have auto-layout in effect.
Instead, you have to add a vertical and horizontal constraint that positions the view from the top/left edge, connect an outlet to the constraint, and then change the constant value on the constraint from code.
I had similar problem , but when i tried to change the origin as suggested in first solution , i got "Expression not assignable" error.
so i solved it with below solution in viewDidLoad().
First save its center in some temp variable , then change the centre and add again to its parent view.
CGPoint buttonCenter = self.yourButton.center;
[self.yourButton removeFromSuperview];
buttonCenter.y = buttonCenter.y + 50.0;
self.yourButton.center = buttonCenter;
[self.view addSubview:self.yourButton];
I'm following along with the Stanford ios7 course, chapter 8, where the instructor builds a simplified Tetris game, with colored blocks dropping from above, where you have to fill in rows. After adding gravity behavior to make the blocks fall, he adds a collision behavior (note the property below), lazily instantiates it and, while doing that, he sets the bounds like this
_collider.translatesReferenceBoundsIntoBoundary = YES;
which makes the blocks collide with the bottom of the screen (rather than falling through) so they can stack on top of each other. He then adds the collision behavior to the animator property, and, as a final step, in the drop method, he adds the dropView (which are the blocks) to the collision behavior. When he runs, the blocks hit the bottom and stack ontop of each other. When I run, using the code below, the blocks continue to fall through the bottom of the screen (on the simulator). In other words, there is no stacking.
Can you see why the collision behavior might not be working.
ViewController
#property (strong,nonatomic) UIDynamicAnimator *animator;
#property (strong, nonatomic) UIGravityBehavior *gravity;
#property (strong, nonatomic) UICollisionBehavior *collider;
#end
#implementation DropItViewController
static const CGSize DROP_SIZE = { 40, 40 };
- (IBAction)tap:(UITapGestureRecognizer *)sender {
[self drop];
}
- (UICollisionBehavior *)collider
{
if (!_collider){
_collider = [[UICollisionBehavior alloc] init];
_collider.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:_collider];
}
return _collider;
}
- (UIDynamicAnimator *)animator
{
if (!_animator) {
_animator = [[UIDynamicAnimator alloc] init];
}
return _animator;
}
-(UIGravityBehavior *)gravity
{
if (!_gravity) {
_gravity = [[UIGravityBehavior alloc] init];
[self.animator addBehavior:_gravity];
}
return _gravity;
}
Add the dropView to the collider in the drop method
-(void)drop
{
CGRect frame;
frame.origin = CGPointZero;
frame.size = DROP_SIZE;
int x = (arc4random() % (int)self.gameView.bounds.size.width) / DROP_SIZE.width;
frame.origin.x = x * DROP_SIZE.width;
UIView *dropView = [[UIView alloc] initWithFrame:frame];
dropView.backgroundColor = self.randomColor;
[self.gameView addSubview:dropView];
[self.gravity addItem:dropView];
[self.collider addItem:dropView];
}
When you instantiate your UIDynamicAnimator, use initWithReferenceView instead of init. Then when you use translatesReferenceBoundsIntoBoundary, it will know what reference bounds to use.
I'm in the same course and thought I was seeing the same issue. However, try running in the 4.0 inch simulator. Your squares are probably collecting just offscreen (outside the bounds of a 3.5 inch screen).
I'm trying to add a vertical parallax scrolling effect between several views that are all the size of the screen.
This wasn't too complicated when done through buttons, however I want it to be controlled by a UIPanGesture, similar to the Year Walk Companion App for iOS. The current equation shown here doesn't work correctly.
Currently this version only moves the first two views, because once I get those working the rest should be easy. I've also not included all the checks that would stop certain views moving in certain directions because that is also not relevant to the issue.
Here is a web version of it working with buttons: http://flycarpodacus.zxq.net/iPhone%20Parallax/event.html
Hope someone can help.
ViewController.h
#interface ViewController : UIViewController {
UIPanGestureRecognizer *panGesture;
int activeView;
int nextView;
float offset;
float speed;
}
#property (weak, nonatomic) IBOutlet UIView *view1;
#property (weak, nonatomic) IBOutlet UIView *view2;
#property (weak, nonatomic) IBOutlet UIView *view3;
#property (weak, nonatomic) IBOutlet UIView *view4;
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize view1, view2, view3, view4;
- (void)viewDidLoad
{
[super viewDidLoad];
activeView = 1;
offset = 220;
speed = 1;
panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanGesture:)];
[self.view addGestureRecognizer:panGesture];
if (self.view.frame.size.height != 568) {
CGRect frame = self.view.frame;
frame.size.height = self.view.frame.size.height;
view1.frame = frame;
view2.frame = frame;
view3.frame = frame;
view4.frame = frame;
}
view4.center = CGPointMake(view4.center.x, self.view.frame.size.height/2 + offset);
view3.center = CGPointMake(view3.center.x, self.view.frame.size.height/2 + offset);
view2.center = CGPointMake(view2.center.x, self.view.frame.size.height/2 + offset);
view1.center = CGPointMake(view1.center.x, self.view.frame.size.height/2);
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint panGestureTranslation = [panGestureRecognizer translationInView:self.view];
switch (activeView) {
case 1:
view1.center = CGPointMake(view1.center.x, panGestureTranslation.y + self.view.frame.size.height/2);
view2.center = CGPointMake(view2.center.x, view2.center.y - 2.5f);
break;
default:
break;
}
}
Ok so, in my point of view you could try to do something like that :
first use only two view, in your example only two view is visible at the same time and so you could re-use the view for
the next slide you want to display.
So at the start you have 2 views (lets say they have the same size than the screen 300x500, yes I know they are not the real size,
but it will be more easy for the math :p).
In you init function place you view one in position 150x250 (the center of the screen), and the screen 2 at 150x500 (so you see only
the upper half of your view).
lets call panGestureTranslation.y : DeltaY (it will be more quick). so in the function panGestureRecognizer you want to move the two view
with the parralex effect, one simple solution in this exact case will be to move view1 of 2*DeltaY and view2 of DeltaY, like that, when the view1
will have quit the screen completly, the view two will be right in the center of it.
"
view1.center = CGPointMake(view1.center.x, view1.center.y + 2*DeltaY);
view2.center = CGPointMake(view1.center.x, view2.center.y + DeltaY);
"
you can know if is finish by test the position of view one (if the position.y is <= than 0, so it's finish.
when is finish, you have two choice, first you put the view1 in the bottom of view2 (at position 150x500 and display behind view2 ) and you feel it with your 3rd content, then switch a booleen
to do the excact opposite the next frame (you will move view2 of 2*DeltaY and view1 of DeltaY).
other solution is to move the two view at the original position (view1 at 150x250 and view two at 150x500), and feel view1 with the content of view2 (you have to do it without animation
so the user doesn't see that the view have switch), and feel view2 with you 3rd content, and so next frame, the exact same code will work again. you can have an infinity of content with this method.
"
view1.center = CGPointMake(view1.center.x, view1.center.y + 2*DeltaY);
view2.center = CGPointMake(view1.center.x, view2.center.y + DeltaY);
"
let me know if I wasn't clear on some point ^^.
I would suggest checking our article on parallax effect with detailed explanation and open source implementation => http://blog.denivip.ru/index.php/2013/08/parallax-in-ios-applications/?lang=en