3

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?

daniel f.
  • 1,421
  • 1
  • 13
  • 24
  • square is the bigger square in the screenshot, just another UIView. I would be very surprused if the is the issue – daniel f. Apr 28 '14 at 17:34
  • Yeah I realised that. The order of the code in your q is backwards so I didn't realise you were adding the circle to it. However, see my answer. – Fogmeister Apr 28 '14 at 17:36
  • 3
    For the benefit of future readers, in iOS 9, this is now possible with `UIDynamicItem` properties `collisionBoundsType` and `collisionBoundingPath`. – Rob Sep 14 '15 at 00:31
  • Thanks @Rob, good to know. Future readers should look into that, at the time I wrote this post, iOS7 was the current version iirc. – daniel f. Sep 14 '15 at 07:22
  • @Rob, You should post that as an answer as it's the actual correct solution to the original question. – Iulian Onofrei Jul 10 '17 at 16:23
  • 1
    @IulianOnofrei Done. – Rob Jul 10 '17 at 18:30

2 Answers2

8

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

}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

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.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Fogmeister
  • 76,236
  • 42
  • 207
  • 306
  • Thanks for your answer, it cleared a few doubts i was having. I am not really trying to do anything complicated, this is just for a homework and i wanted to present something nice. I do not think i would have used uikit dynamics for something more complicated than that. Your point of this kit being a convenience for nicer animations makes a lot of sense, and explains some of its shortcomings. I will link into the sprite kit at a later point! Thanks for your help – daniel f. Apr 29 '14 at 12:22
  • No worries. You can get some really nice effects with dynamics within certain constraints. I'd recommend looking at Ray Wenderlich site for sprite kit. Really easy to get advanced physics in there. – Fogmeister Apr 29 '14 at 12:50
  • 1
    This question was not about `Sprite Kit`. – Iulian Onofrei Jul 10 '17 at 16:22
  • 1
    @IulianOnofrei this question is 3 years old. My answer is too. What is your point? You've resurrected a dead question. Yes, it might be good for Rob to post his comment as an answer. Does it make my answer wrong? No. The OP was trying to do something with UIKitDynamics that it was not designed for. So I said that they would be better off using SpriteKit. I'd expect anyone to do the same for me if I was trying to do something in a way that was not the ideal way. Maybe the op didn't know about SpriteKit? Did you think about that at all? Or were you just trying to be rude? – Fogmeister Jul 10 '17 at 16:28
  • I think you should add your comment as an edit @Fogmeister. People might not be further than after your answer as it is the accepted one, when they actually should see Rob's one. – Tulleb Mar 06 '18 at 10:29