Question

i am writing an App where i use UIKit Dynamics to simulate the interactions of different circles with one another.

I create my circles with the following code:

self = [super initWithFrame:CGRectMake(location.x - radius/2.0, location.y - radius/2, radius, radius)];
if (self) {
    [self.layer setCornerRadius: radius /2.0f];
    self.clipsToBounds = YES;
    self.layer.masksToBounds = YES;
    self.backgroundColor = color;
    self.userInteractionEnabled = NO;
}
return self;

where location represents the desired location of the circle, and radius its radius.

I then add these circles to different UIBehaviours, by doing:

[_collision addItem:circle];
[_gravity addItem:circle];
[_itemBehaviour addItem:circle];

The itemBaviour is defined as follows:

_itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[square]];
_itemBehaviour.elasticity = 1;
_itemBehaviour.friction = 0;
_itemBehaviour.resistance = 0;
_itemBehaviour.angularResistance = 0;
_itemBehaviour.allowsRotation = NO;

The problem i am having, is that my circles are behaving as squares. When hit in certain ways they gain angular momentum and lose speed. If they collide again, sometimes the angular momentum is again reverted to speed. This looks normal for squares, but when the view is round, like in my case, this behaviour looks weird and unnatural.

Turning on some debug options, i made this screenshot: small circle is actually a square

As you can see, the circle is appearently a square.

So my question is, how can i create an UIVIew that is truly a circle and will behave as such in UIKit Dynamics?

Was it helpful?

Solution

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:

enter image description here

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)
    }

}

OTHER TIPS

Ok, well first off.

The debug options you enabled show areas of transparent cells. The view that is the circle is actually a square with rounded edges.

All views are rectangular. The way they appear circular is by making the corners transparent (hence corner radius).

Second, what is it you're trying to do with UIKit Dynamics? What is on the screen looks like you're trying to create a game of some sort.

Dynamics is meant to be used for more natural and real looking animation of UI. It isn't meant to be a full-on physics engine.

If you want something like that then you're best using Sprite Kit.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top