Swift Uikit Dynamics Add Collision Boundary. After Rotation Does Not Work Correctly

swift UIBezierPath collision boundary issues ( UIKit Dynamics )

UIKit Dynamics does not permit concave paths. But you can add them as separate UICollisionBehavior instances:

for index in points.indices.dropFirst() {
let path = UIBezierPath()
path.move(to: points[index-1])
path.addLine(to: points[index])

let collision = UICollisionBehavior(items: [droppedView])
collision.addBoundary(withIdentifier: "\(index)" as NSString, for: path)
animator.addBehavior(collision)
}

That results in:

Sample Image

Swift UIKit dynamics - Vary UIPushBehavior force magnitude by distance from center of source

Use a UIFieldBehaviour to create a linear gravitational field in the direction of the fan. Then you can specify a falloff:

var forceField: UIFieldBehaviour!

// ...

forceField = UIFieldBehavior.linearGravityField(direction: CGVector(dx: 1, dy: -5))

// example values for a "fan" on the bottom left blowing mostly upwards:
forceField.position = CGPoint(x: view.bounds.minX, y: view.bounds.maxY)
forceField.region = UIRegion(radius: 3000)
forceField.minimumRadius = 100
forceField.falloff = 5
forceField.strength = 10
forceField.addItem(view1)
forceField.addItem(view2)
animator.addBehavior(forceField)

Have fun playing around with these values!

Adding collision, another gravity behaviour, and a dynamic item behaviour to two views, we get the following effect:

Sample Image

That feels like a fan on the bottom left to me!

You can also choose a radial gravitational field positioned at where the fan is, if your fan is in a corner and blows "radially", but note that you should use a negative value for strength in that case to say that the field repels rather than attracts.

UIKit Dynamics: recognize rounded Shapes and Boundaries

I know this question predated iOS 9, but for the benefit of future readers, you can now define a view with collisionBoundsType of UIDynamicItemCollisionBoundsTypePath and a circular collisionBoundingPath.

So, while you cannot "create an UIView that is truly a circle", you can define a path that defines both the shape that is rendered inside the view as well as the collision boundaries for the animator, yielding an effect of a round view (even though the view, itself, is obviously still rectangular, as all views are):

@interface CircleView: UIView

@property (nonatomic) CGFloat lineWidth;
@property (nonatomic, strong) CAShapeLayer *shapeLayer;

@end

@implementation CircleView

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self configure];
}
return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configure];
}
return self;
}

- (instancetype)init {
return [self initWithFrame:CGRectZero];
}

- (void)configure {
self.translatesAutoresizingMaskIntoConstraints = false;

// create shape layer for circle

self.shapeLayer = [CAShapeLayer layer];
self.shapeLayer.strokeColor = [[UIColor blueColor] CGColor];
self.shapeLayer.fillColor = [[[UIColor blueColor] colorWithAlphaComponent:0.5] CGColor];
self.lineWidth = 3;
[self.layer addSublayer:self.shapeLayer];
}

- (void)layoutSubviews {
[super layoutSubviews];

// path of shape layer is with respect to center of the `bounds`

CGPoint center = CGPointMake(self.bounds.origin.x + self.bounds.size.width / 2, self.bounds.origin.y + self.bounds.size.height / 2);
self.shapeLayer.path = [[self circularPathWithLineWidth:self.lineWidth center:center] CGPath];
}

- (UIDynamicItemCollisionBoundsType)collisionBoundsType {
return UIDynamicItemCollisionBoundsTypePath;
}

- (UIBezierPath *)collisionBoundingPath {
// path of collision bounding path is with respect to center of the dynamic item, so center of this path will be CGPointZero

return [self circularPathWithLineWidth:0 center:CGPointZero];
}

- (UIBezierPath *)circularPathWithLineWidth:(CGFloat)lineWidth center:(CGPoint)center {
CGFloat radius = (MIN(self.bounds.size.width, self.bounds.size.height) - self.lineWidth) / 2;
return [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:true];
}

@end

Then, when you do your collision, it will honor the collisionBoundingPath values:

self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];

// create circle views

CircleView *circle1 = [[CircleView alloc] initWithFrame:CGRectMake(60, 100, 80, 80)];
[self.view addSubview:circle1];

CircleView *circle2 = [[CircleView alloc] initWithFrame:CGRectMake(250, 150, 120, 120)];
[self.view addSubview:circle2];

// have them collide with each other

UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[circle1, circle2]];
[self.animator addBehavior:collision];

// with perfect elasticity

UIDynamicItemBehavior *behavior = [[UIDynamicItemBehavior alloc] initWithItems:@[circle1, circle2]];
behavior.elasticity = 1;
[self.animator addBehavior:behavior];

// and push one of the circles

UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:@[circle1] mode:UIPushBehaviorModeInstantaneous];
[push setAngle:0 magnitude:1];

[self.animator addBehavior:push];

That yields:

Sample Image

By the way, it should be noted that the documentation outlines a few limitations to the path:

The path object you create must represent a convex polygon with counter-clockwise or clockwise winding, and the path must not intersect itself. The (0, 0) point of the path must be located at the center point of the corresponding dynamic item. If the center point does not match the path’s origin, collision behaviors may not work as expected.

But a simple circle path easily meets those criteria.


Or, for Swift users:

class CircleView: UIView {
var lineWidth: CGFloat = 3

var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
_shapeLayer.strokeColor = UIColor.blue.cgColor
_shapeLayer.fillColor = UIColor.blue.withAlphaComponent(0.5).cgColor
return _shapeLayer
}()

override func layoutSubviews() {
super.layoutSubviews()

layer.addSublayer(shapeLayer)
shapeLayer.lineWidth = lineWidth
let center = CGPoint(x: bounds.midX, y: bounds.midY)
shapeLayer.path = circularPath(lineWidth: lineWidth, center: center).cgPath
}

private func circularPath(lineWidth: CGFloat = 0, center: CGPoint = .zero) -> UIBezierPath {
let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
}

override var collisionBoundsType: UIDynamicItemCollisionBoundsType { return .path }

override var collisionBoundingPath: UIBezierPath { return circularPath() }
}

class ViewController: UIViewController {

let animator = UIDynamicAnimator()

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

let circle1 = CircleView(frame: CGRect(x: 60, y: 100, width: 80, height: 80))
view.addSubview(circle1)

let circle2 = CircleView(frame: CGRect(x: 250, y: 150, width: 120, height: 120))
view.addSubview(circle2)

animator.addBehavior(UICollisionBehavior(items: [circle1, circle2]))

let behavior = UIDynamicItemBehavior(items: [circle1, circle2])
behavior.elasticity = 1
animator.addBehavior(behavior)

let push = UIPushBehavior(items: [circle1], mode: .instantaneous)
push.setAngle(0, magnitude: 1)
animator.addBehavior(push)
}

}

UICollisionBehavior - views pass through boundaries

Good news: you aren't doing anything wrong. Bad news: this is expected behavior. Your hammer is pushing the view against the reference boundaries until it finally breaks through because the physics of your model says that's what it should do. The reference boundary is not a boundary that can't be crossed, it only defines an area where your physics rules apply consistently.

This isn't really documented, but the page for UICollisionBehavior states:

When setting the initial position for a dynamic item, you must ensure
that its bounds do not intersect any collision boundaries. The
animation behavior for such a misplaced item is undefined.

This is only partially true, in your case (as with mine) if an item goes outside the boundary after initialization, it's behavior is also undefined.

I tried to arrange a set of balls on the right side of a view. There is an anchor point under the rightmost ball. The yellow view is the reference view and everything starts off fine...
this works

But, as I added more balls to they point they could no longer fit, they would start to pop out. In fact, the one on the top right of the image popped out of the bottom center and rolled around the reference view counter clockwise to be near the anchor point.
this is broken

UPDATE:
For a solution you can set the collisionDelegate of your collisionBehaviors and capture the start and ends of the collisions and then move your view back into the boundary and away from your hammer to make it look like they escaped.

As you have figured out translatesReferenceBoundsIntoBoundary is equivalent to the set of addBoundaryWithIdentifier calls.

Use:

collisionBehaviour!.translatesReferenceBoundsIntoBoundary = true 

same as:

//add boundaries
collisionBehaviour!.addBoundaryWithIdentifier("verticalMin", fromPoint: CGPointMake(0, 0), toPoint: CGPointMake(0, CGRectGetHeight(view.frame)))
collisionBehaviour!.addBoundaryWithIdentifier("verticalMax", fromPoint: CGPointMake(CGRectGetMaxX(view.frame), 0), toPoint: CGPointMake(CGRectGetMaxX(view.frame), CGRectGetHeight(view.frame)))
collisionBehaviour!.addBoundaryWithIdentifier("horizontalMin", fromPoint: CGPointMake(0, CGRectGetMaxY(view.frame)), toPoint: CGPointMake(CGRectGetMaxX(view.frame), CGRectGetMaxY(view.frame)))
collisionBehaviour!.addBoundaryWithIdentifier("horizontalMax", fromPoint: CGPointMake(0, 0), toPoint: CGPointMake(CGRectGetMaxX(view.frame), 0))


Related Topics



Leave a reply



Submit