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:
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:
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:
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...
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.
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
How to Call Swiftui Navigationlink Conditionally
Create a Weak Container in Swift That Accepts a Native Swift Protocol
What the Difference of Keys and Values in Dictionary of Swift
How to Integrate Mapbox Sdk with Swiftui
Swift: How to Invalidate a Timer If the Timer Starts from a Function
How to Filter Nsarray in Swift
Datefromstring() Returns Incorrect Date
Convert Swift Encodable Class Typed as Any to Dictionary
Switch Statement for Imported Ns_Options (Rawoptionsettype) in Swift
Accessing Struct from One Class to Another
Swift Access Array with Index Gives Following Error. Any Idea Why
Rotation Gesture Produces Undesired Rotation
Swift Generics and Protocols Not Working on Uikit [Possible Bug]
Swift 2: Multiline Mkpointannotation
Uitextview Linktextattributes Font Attribute Not Applied to Nsattributedstring