UICollisionBehavior - custom shape for UIView collision - ios

I'm trying to figure out how to use UIKit Dynamics to successfully collide two UIViews which have custom boundary shapes.
The most basic example I can think of to explain my question is to have two circles collide (taking in to account their round corners) instead of their square boundary.
I'm sure I've seen this somewhere but I can't find any documentation or discussion on the subject from any official source.

I'd like to do this too, but I don't think you can do it under the current UIKit Dynamics for iOS 7. Items added to the animator must adopt the UIDynamicItem protocol (UIView does). The protocol only specifies their boundaries to be rectangular, via the bounds property, which is a CGRect. There's no custom hit test.
However, you can add a fixed Bezier path to the collision set, and it could be circular or any shape you can make with a path, but it would act like a curved wall that other rectangular objects bounce off of. You can modify the DynamicsCatalog sample code in Xcode to see the use of a curved boundary which doesn't move.
Create a new view file called BumperView, subclass of UIView.
In BumperView.m, use this drawRect:
#define LINE_WIDTH 2.0
- (void)drawRect:(CGRect)rect
{
UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, LINE_WIDTH/2, LINE_WIDTH/2)];
[[UIColor blueColor] setStroke];
[[UIColor lightGrayColor] setFill];
ovalPath.lineWidth = LINE_WIDTH;
[ovalPath stroke];
[ovalPath fill];
}
In the storyboard for the Item Properties page, add a View somewhere below the boxes and change its class to BumperView, and change its background color to clear.
Create an outlet named bumper to it in APLItemPropertiesViewController.m, but give it class BumperView.
Add the following in the viewDidAppear function, after collisionBehavior has been created:
UIBezierPath *bumperPath = [UIBezierPath bezierPathWithOvalInRect:self.bumper.frame];
[collisionBehavior addBoundaryWithIdentifier:#"Bumper" forPath:bumperPath];
Run it and go to the Item Properties page to see the rectangles bounce off the oval.

Related

Setting collision bounding path of a UIView in iOS 9

In iOS 9 Apple introduced the collisionBoundsType to UIKit-Dynamics.
I have no issue when setting this UIDynamicItemCollisionBoundsTypeRectangle or when I set this to UIDynamicItemCollisionBoundsTypeEllipse.
The screenshot below is from a game I am making where the collisionBoundsType of the player is set to rectangle and the ball is set to ellipse:
However, when I set the player's collisionBoundsType to path I get weird behavior as seen here:
The view appears higher than it should and the collision body is to the right of where it should be.
Currently I have collisionBoundingPath set to this:
- (UIBezierPath *)collisionBoundingPath
{
maskPath = [[UIBezierPath alloc] init];
[maskPath addArcWithCenter:CGPointMake(SLIME_SIZE, SLIME_SIZE) radius:SLIME_SIZE startAngle:0*M_PI endAngle:M_PI clockwise:NO];
return maskPath;
}
Additionally, my drawRect function looks like this:
- (void) drawRect:(CGRect)rect
{
if (!_color){
[self returnDefualtColor];
}
if (!maskPath) maskPath = [[UIBezierPath alloc] init];
[maskPath addArcWithCenter:CGPointMake(SLIME_SIZE, SLIME_SIZE) radius:SLIME_SIZE startAngle:0*M_PI endAngle:M_PI clockwise:NO];
[_color setFill];
[maskPath fill];
}
Why is this happening? How do I set the path of the collision body to be the same as the drawing in the view?
Additionally, the red is just the background of the view (i.e. view.backgroundColor = [UIColor redColor];).
From the documentation on the UIDynamicItem here, the following statement about the coordinate system for paths seems to represent what is wrong:
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.
Here it states that the (0,0) for the path MUST be the center point.
I would think that the center of your arc path should be (0,0) and not (SLIME_SIZE/2,SLIME_SIZE/2). Have you perhaps set the width and height of the UIView frame to SLIME_SIZE rather than SLIME_SIZE*2?
SLIME_SIZE really seems to define the radius, so the frame width should be SLIME_SIZE*2. If it is set as SLIME_SIZE, then that would explain why you need to translate by SLIME_SIZE/2 as a correction.
I was able to answer this by changing:
- (UIBezierPath *)collisionBoundingPath
{
maskPath = [[UIBezierPath alloc] init];
[maskPath addArcWithCenter:CGPointMake(SLIME_SIZE, SLIME_SIZE) radius:SLIME_SIZE startAngle:0*M_PI endAngle:M_PI clockwise:NO];
return maskPath;
}
to:
- (UIBezierPath *)collisionBoundingPath
{
maskPath = [[UIBezierPath alloc] init];
[maskPath addArcWithCenter:CGPointMake(SLIME_SIZE / 2, SLIME_SIZE / 2) radius:SLIME_SIZE startAngle:0*M_PI endAngle:M_PI clockwise:NO];
return maskPath;
}
The key difference is that I modified the center of the arc by dividing the x and y values by 2.
Debugging physics is a thing. It's probably not something that iOS users have tended to think a lot about as they've generally done very simple things with UIKit Dynamics. This is a bit of a shame, as it's one of the best aspects of the recent editions of iOS, and offers a truly fun way to make compelling user experiences.
So... how to debug physics?
One way is to mentally imagine what's going on, and then correlate that with what's going on, and find the dissonance between the imagined and the real, and then problem solve via a blend of processes of elimination, mental or real trial & error and deduction, until the problem is determined and solved.
Another is to have a visual depiction of all that's created and interacting presenting sufficient feedback to more rapidly determine the nature and extents of elements, their relationships and incidents/events, and resolve issues with literal sight.
To this end, various visual debuggers and builders of physics simulations have been created since their introduction.
Unfortunately iOS does not have such a screen based editor or "scene editor" for UIKit Dynamics, and what is available for this sort of visual debugging in Sprite Kit and Scene Kit is rudimentary, at best.
However there's CALayers, which are present in all UIKit Views, into which CAShapeLayers can be manually created and drawn to accurately represent any and all physical elements, their bounds and their anchors and relationships.
CAShapeLayers are a "container" for CGPaths, and can have different colours for outline and fill, and more than one CGPath element within a single CAShapeLayer.
And, to quote the great Rob:
"If you add a CAShapeLayer as a layer to a view, you don't have to
implement any drawing code yourself. Just add the CAShapeLayer and
you're done. You can even later change the path, for example, and it
will automatically redraw it for you. CAShapeLayer gets you out of the
weeds of writing your own drawRect or drawLayer routines."
If you have an enormous number of interacting elements and want to debug them, CAShapeLayer's performance issues might come into play, at which point you can use shouldRasterize to convert each to a bitmap, and get a significant performance improvement when hitting limits created by the "dynamic" capabilities of CAShapeLayers.
Further, for representing things like constraints and joints, there's a simple process of created dashed lines on CAShapeLayers, by simply setting properties. Here's the basics of setting up a CAShapeLayer's properties, and the way to use an array to create a 5-5-5 dashed outline with a block stroke, width of 3, no fill.
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
[shapeLayer setBounds:self.bounds];
[shapeLayer setPosition:self.center];
[shapeLayer setFillColor:[[UIColor clearColor] CGColor]];
[shapeLayer setStrokeColor:[[UIColor blackColor] CGColor]];
[shapeLayer setLineWidth:3.0f];
[shapeLayer setLineJoin:kCALineJoinRound];
[shapeLayer setLineDashPattern:
[NSArray arrayWithObjects:[NSNumber numberWithInt:10],
[NSNumber numberWithInt:5],nil]];

iOS Custom UIView Drawing - CAShapeLayers or drawRect?

I'm new to iOS UIView drawing and I'm really trying to implement a custom UIView class in a standard way.
The UIView class that I'm working on now is simple: it has mostly static background shapes that form a composite shape. I also want to add animation to this UIView class so that a simple shape can animate its path on top of the static shapes.
How I'm currently implementing this
Right now, I'm implementing the static drawing in the drawRect: method of the UIView subclass. This works fine. I also have not implemented animation of the single shape yet.
What I'm looking to have answered
Question 1:
Is it better to have the static shapes drawn in the drawRect: method as I'm currently doing it, or is there a benefit to refactoring all of the shapes being drawn into class-extension-scoped CAShapeLayer properties and doing something like:
-(instancetype) initWithFrame:(CGRect) frame
{
if (self = [super initWithFrame:frame])
{
[self setupView];
}
return self;
}
-(void) setupView // Allocate, init, and setup one-time layer properties like fill colorse.
{
self.shapeLayer1 = [CAShapeLayer layer];
[self.layer addSubLayer:shapeLayer1];
self.shapeLayer2 = [CAShapeLayer layer];
[self.layer addSubLayer:shapeLayer2];
// ... and so on
}
-(void) layoutSubviews // Set frames and paths of shape layers here.
{
self.shapeLayer1.frame = self.bounds;
self.shapeLayer2.frame = self.bounds;
self.shapeLayer1.path = [UIBezierPath somePath].CGPath;
self.shapeLayer2.path = [UIBezierPath somePath].CGPath;
}
Question 2:
Regardless of whether I implement the static shapes as CAShapeLayers or in drawRect:, is the best way to implement an animatable shape for this UIView a CAShapeLayer property that is implemented as the other CAShapeLayers would be implemented above? Creating a whole separate UIView class just for one animated shape and adding it as a subview to this view seems silly, so I'm thinking that a CAShapeLayer is the way to go to accomplish that.
I would appreciate any help with this very much!
You could do it either way, but using shape layers for everything would probably be cleaner and faster. Shape layers have built-in animation support using Core Animation.
It's typically faster to avoid implementing drawRect and instead let the system draw for you.
Edit:
I would actually word this more strongly than I did in 2014. The underlying drawing engine in iOS is optimized for tiled rendering using layers. You will get better performance and smoother animation by using CALayers and CAAnimation (or higher level UIView or SwiftUI animation which is built on CAAnimation.)

Ideal choice for a view which recognizes taps and can change its text/label?

This question is for the iOS development platform.
I am looking for something that will have some text on it and will recognize taps. Once tapped, it will do some stuff and change the text on itself. (It will be a part of a game like app).
At first I considered using a UIButton with a custom image. But on trying to change the text on it, I realized that it is read-only.
Then I considered using a UILabel, but it didn't suit my use as, at a later stage, I would like to make the View have a custom shape with an outline.
Right now, I am considering adding a generic UIView and add a Label as a subView to it. (Can I give the UIView a custom shape?)
Please can someone point out a better solution or if I am wrong above.
For creating custom shaped objects use UIbezierPath. Through UIbezierPath we can create custom shaped UIViews. Touch inside those views can handle adding gestures to that views.
Sample:-
UIBezierPath* path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 0)];
[path addLineToPoint:CGPointMake(view.frame.size.width, 0)];
[path addLineToPoint:CGPointMake(view.frame.size.width, [self headerBackGroundView].frame.size.height)];
[path addLineToPoint:CGPointMake(self.frame.size.width, self.frame.size.height)];
[path setLineWidth:1.0];
[[UIColor lightGrayColor] setStroke];
[path stroke];
As Desdenova said, his solution worked for me.
UIButton's title isn't read only. You can use setTitle:forState: method.

How to draw inside a layer and add to context only after [viewLayer addSublayer:newLayer]?

The question
How do you create a separate context for your layer and incorporate that context into super layer?
Why I want to do this
I want abstract the drawing of a view layers into separate objects/files. I want to construct a view out of layers, then position then on top of one another and have other possibilities as that.
The problem is that I'm not aware of you you're supposed to draw a part of your view into a layer without drawing straight into the context of the main views sublayer.
Here's an example, I have subclassed CALayer with HFFoundation:
#implementation HFFoundation
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
UIBezierPath *foundation = [UIBezierPath bezierPath];
// some drawing goes on here in the form of messages to [foundation]
}
#end
Now when I instantiate my custom layer inside a views (void)drawRect:(CGRect)rect I this way:
HFFoundation *foundation = [HFFoundation new];
foundation.frame = CGRectMake(40, viewHeight - 100, 300, 100);
foundation.backgroundColor = [[UIColor colorWithRed:1 green:.4 blue:1 alpha:0.2] CGColor];
[self.window.rootViewController.view.layer addSublayer:foundation];
I get this result (no drawings appear, just the bg color inside the layer frame):
If I [foundation drawLayer:foundation inContext:context]; afterwards, the drawing appears, but it appears inside the top layers context, not in foundation layer. Since foundation layer is also lower in the hierarchy, it hides the drawing (unless I reduce it's alpha, which I've dont in the picture):
How do you draw into the layer itself, I.e. foundation in this case?
To insert a sublayer and set its index you want this piece of code:
[view.layer insertSublayer:foundation atIndex:0];

Drawing outside a UIView

I have a UIView where I would like to draw a Circle that extends past the frame of the UIView,
I have set the masksToBounds to NO - expecting that I can draw past outside the bounds of the UIView by 5 pixels on the right and bottom.
I expect the oval to not get clipped but it does get clipped and does not draw outside the bounds?
- (void)drawRect:(CGRect)rect
{
int width = self.bounds.size.width;
int height = self.bounds.size.height;
self.layer.masksToBounds = NO;
//// Rounded Rectangle Drawing
//// Oval Drawing
UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(0, 0, width+5, height+5)];
[[UIColor magentaColor] setFill];
[ovalPath fill];
[[UIColor blackColor] setStroke];
ovalPath.lineWidth = 1;
[ovalPath stroke];
}
From http://developer.apple.com/library/ios/#documentation/general/conceptual/Devpedia-CocoaApp/DrawingModel.html
UIView and NSView automatically configure the drawing environment of a
view before its drawRect: method is invoked. (In the AppKit framework,
configuring the drawing environment is called locking focus.) As part
of this configuration, the view class creates a graphics context for
the current drawing environment.
This graphics context is a Quartz object (CGContext) that contains
information the drawing system requires, such as the colors to apply,
the drawing mode (stroke or fill), line width and style information,
font information, and compositing options. (In the AppKit, an object
of the NSGraphicsContext class wraps a CGContext object.) A graphics
context object is associated with a window, bitmap, PDF file, or other
output device and maintains information about the current state of the
drawing environment for that entity. A view draws using a graphics
context associated with the view’s window. For a view, the graphics
context sets the default clipping region to coincide with the view’s
bounds and puts the default drawing origin at the origin of a view’s
boundaries.
Once the clipping region is set, you can only make it smaller. So, what you're trying to do isn't possible in a UIView drawRect:.
I'm not certain this will fix your problem, but it's something to look into. You're setting self.layer.masksToBounds = NO every single time you enter drawRect. You should try setting it inside the init method just once instead, A) because it's unnecessary to do it multiple times and B) because maybe there's a problem with setting it after drawRect has already been called--who knows.

Resources