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).
Related
I work in Xcode 7, application for IOS 9. The goal is to get round objects behave like a balls using UIDynamicAnimator, UIGravityBehavior and UICollisionBehavior.
Everything works fine except that UIViews (representing balls) looks rounded but behave during collision like rectangles.
Code that build and adds 'balls' is:
-(void) addBall{
CGRect frame;
frame.origin=CGPointZero;
frame.size = [self randomsize];
int x = (arc4random()%(int)self.inputView.bounds.size.width);
frame.origin.x = x;
UIView *bullet = [[UIView alloc] initWithFrame:frame];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10,10,40,20)];
label.text = #"label";
bullet.backgroundColor = [self randomColor];
bullet.layer.cornerRadius = bullet.frame.size.width/2;
bullet.layer.borderWidth = 2.0;
bullet.layer.borderColor = [UIColor blackColor].CGColor;
bullet.layer.masksToBounds=YES;
[bullet addSubview:label];
[self.inputView addSubview:bullet];
[self.gravity addItem:bullet];
[self.collider addItem:bullet];
}
Which property needs to be set to get those objects behave like round shapes?
Below is the screen of the app showing how gravity and collision affected shapes lay on the ground/border.
I draw rectangular borders on the screenshot to show real border that are invisible but prevents balls from behaving as rounded objects.
You should subclass UIView and overload collisionBoundsType. Something like:
-(UIDynamicItemCollisionBoundsType) collisionBoundsType
{
return UIDynamicItemCollisionBoundsTypeEllipse;
}
In Swift you can just make an extension instead of creating a subclass which may be easier than beyowulfs answer, can just drop it at the bottom of your class if you just need it once off
extension UIView {
func collisionBoundsType () -> (UIDynamicItemCollisionBoundsType) {
return UIDynamicItemCollisionBoundsType.ellipse
}
}
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];
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];
I'd like to make it when the user shakes the device the ball rattles around inside the object on screen. I'm assuming I need to set up an invisible box for it to collide with. It doesn't matter if it moved randomly or follows a predefined path, whichever is easiest.
I think I understand the "Activate on shake" part of the code, just the ball/object movement I'm not sure of
This should work:
//You need an #property (nonatomic, strong) UIDynamicAnimator *animator; in your .h
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.viewToBounceAroundIn];
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:#[self.viewThatBouncesAround]];
collision.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:collision];
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:#[self.viewThatBouncesAround] mode:UIPushBehaviorModeInstantaneous];
push.magnitude = 1; //Play with this, it's how much force is applied to your object
push.angle = 0; //play with this too
[self.animator addBehavior:push];
I typed this away from a compiler - let me know if it works. The idea is that you use UIKitDynamics as a physics engine, use a UICollisionBehavior to let the item bounce around inside the box, and a UIPushBehavior to apply the initial force.
If the item slows down too quickly for you, or loses too much energy when it bounces off walls, you can adjust its properties:
UIDynamicItemBehavior *behavior = [[UIDynamicItemBehavior alloc] initWithItems:#[self.itemThatBouncesAround]];
behavior.friction = 0; //no friction. play with this.
behavior.elasticity = 1;; //completely elastic, play with this.
[self.animator addBehavior:behavior];
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