I work in Xcode 7, application for IOS 9. The goal is to get round objects behave like a balls using UIDynamicAnimator, UIGravityBehavior and UICollisionBehavior.
Everything works fine except that UIViews (representing balls) looks rounded but behave during collision like rectangles.
Code that build and adds 'balls' is:
-(void) addBall{
CGRect frame;
frame.origin=CGPointZero;
frame.size = [self randomsize];
int x = (arc4random()%(int)self.inputView.bounds.size.width);
frame.origin.x = x;
UIView *bullet = [[UIView alloc] initWithFrame:frame];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10,10,40,20)];
label.text = #"label";
bullet.backgroundColor = [self randomColor];
bullet.layer.cornerRadius = bullet.frame.size.width/2;
bullet.layer.borderWidth = 2.0;
bullet.layer.borderColor = [UIColor blackColor].CGColor;
bullet.layer.masksToBounds=YES;
[bullet addSubview:label];
[self.inputView addSubview:bullet];
[self.gravity addItem:bullet];
[self.collider addItem:bullet];
}
Which property needs to be set to get those objects behave like round shapes?
Below is the screen of the app showing how gravity and collision affected shapes lay on the ground/border.
I draw rectangular borders on the screenshot to show real border that are invisible but prevents balls from behaving as rounded objects.
You should subclass UIView and overload collisionBoundsType. Something like:
-(UIDynamicItemCollisionBoundsType) collisionBoundsType
{
return UIDynamicItemCollisionBoundsTypeEllipse;
}
In Swift you can just make an extension instead of creating a subclass which may be easier than beyowulfs answer, can just drop it at the bottom of your class if you just need it once off
extension UIView {
func collisionBoundsType () -> (UIDynamicItemCollisionBoundsType) {
return UIDynamicItemCollisionBoundsType.ellipse
}
}
Related
I understand that the question might sound too simple, but only after struggling for several hours with this simple thing am I posting this here. (Yup, I am new to custom drawing and iOS dev in general).
I am trying to draw a coin. It is a gray circle with some text centered on it.
What I have tried so far:
CGFloat *radius = 30;
CGPoint center = self.view.center;
CGRect someFrame = CGRectMake(center.x - radius, center.y - radius,
2 * radius, 2 * radius);
UIView *circularView = [[UIView alloc] initWithFrame:someFrame];
circularView.backgroundColor = [UIColor grayColor];
UILabel *label = [[UILabel alloc] initWithFrame:someFrame];
label.text = #"5";
label.textColor = [UIColor blackColor];
label.textAlignment = NSTextAlignmentCenter;
[circularView addSubview:label];
circularView.clipsToBounds = YES;
circularView.layer.cornerRadius = radius;
[self.view addSubview:circularView];
Have tried this and other variants. But none of it is working. The label is not appearing, and the view is not being drawn in the center of the view as expected in landscape mode.
Is there any better way to do this, using CALayer or Quartz or Core Graphics? As I said, I am new to this.
First things first:
CGFloat *radius = 30;
That...shouldn't really even compile. Or at least there should be one mean warning. That's not what you want. You want to say this:
CGFloat radius = 30;
That asterisk is going to, just, that's bad. You'll know when you want to use a pointer to a primitive value and this just ain't one o' those times.
With that outta the way, you're on the right track, but it looks like you have a misunderstanding about coordinate spaces.
You initialize the circle with the frame someFrame. This is a frame that makes sense in the coordinate space of self.view, which is good, because that's where you're adding it (mostly, see side note below).
But then you set the label's frame to the same thing. Which might be okay, except that you're adding it to the circularView -- not to self.view. Which means that the frame that made sense as a frame for a child of self.view suddenly doesn't make very much sense at all. What you really want is just the following:
// The label will take up the whole bounds of the circle view.
// Labels automatically center their text vertically.
UILabel *label = [[UILabel alloc] initWithFrame:circularView.bounds];
// Center the text in the label horizontally.
label.textAlignment = NSTextAlignmentCenter;
// Make it so that the label's frame is tied to the bounds of
// the circular view, so that if its size changes in the future
// the label will still look right.
label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
The problem that you have now is that label is way off somewhere else, sticking off the edge of circularView (and thus getting clipped) because someFrame doesn't make sense in circularView's coordinate space.
Side note:
This won't work well if self.view.frame.origin isn't CGPointZero. It usually is, but what you really want is the center of the view's bounds, not the center of the view's frame. self.view.center gives you a point in the coordinate space of self.view.superview. It just so happens that this will appear to work as long as self.view.frame.origin is {0, 0}, but to be more technically correct, you should say:
CGPoint center = CGPointMake(CGRectGetMidX(self.view.bounds),
CGRectGetMidY(self.view.bounds));
You can also make circularView remain in the center of the view even as the view's bounds change (for example, if you rotate the device), with the following:
circularView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleBottomMargin;
Yes, typing out autoresizingMasks manually is the worst thing ever, but it's worth it in the long run (actually, in the long run, you'll probably go crazy and make a shorter helper method like me...).
You need to change the origin of the label's frame. When you add the label to the background view, the origin of its frame is relative to its superview.
So it would be something like CGRectMake(center.x - radius, center.y - radius, 2 * radius, 2 * radius) for the background view, CGRectMake(0, 0, 2 * radius, 2 * radius) for the label.
Besides that, you can skip the background view and tint the UILabel's background color.
Your first problem is that you are creating a frame for the view container (circularView) with some non zero x and y. Then you are creating a label and giving it the same frame (with non zero x and y). Then you add the label to the container view. The label's x and y will be relative to the container view's x and y (offset, not centered). If you want the label and the container to share the same location on screen then change it to this:
UILabel *label = [[UILabel alloc] initWithFrame: circularView.bounds];
Another problem is with how simple you are making this you could do it all with the label (ignore the container view).
UILabel *label = [[UILabel alloc] initWithFrame:someFrame];
label.text = #"5";
label.textColor = [UIColor blackColor];
label.textAlignment = NSTextAlignmentCenter;
label.clipsToBounds = YES;
label.layer.cornerRadius = radius;
label.backgroundColor = [UIColor grayColor];
]self.view addSubview:label];
I'm following along with the Stanford ios7 course, chapter 8, where the instructor builds a simplified Tetris game, with colored blocks dropping from above, where you have to fill in rows. After adding gravity behavior to make the blocks fall, he adds a collision behavior (note the property below), lazily instantiates it and, while doing that, he sets the bounds like this
_collider.translatesReferenceBoundsIntoBoundary = YES;
which makes the blocks collide with the bottom of the screen (rather than falling through) so they can stack on top of each other. He then adds the collision behavior to the animator property, and, as a final step, in the drop method, he adds the dropView (which are the blocks) to the collision behavior. When he runs, the blocks hit the bottom and stack ontop of each other. When I run, using the code below, the blocks continue to fall through the bottom of the screen (on the simulator). In other words, there is no stacking.
Can you see why the collision behavior might not be working.
ViewController
#property (strong,nonatomic) UIDynamicAnimator *animator;
#property (strong, nonatomic) UIGravityBehavior *gravity;
#property (strong, nonatomic) UICollisionBehavior *collider;
#end
#implementation DropItViewController
static const CGSize DROP_SIZE = { 40, 40 };
- (IBAction)tap:(UITapGestureRecognizer *)sender {
[self drop];
}
- (UICollisionBehavior *)collider
{
if (!_collider){
_collider = [[UICollisionBehavior alloc] init];
_collider.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:_collider];
}
return _collider;
}
- (UIDynamicAnimator *)animator
{
if (!_animator) {
_animator = [[UIDynamicAnimator alloc] init];
}
return _animator;
}
-(UIGravityBehavior *)gravity
{
if (!_gravity) {
_gravity = [[UIGravityBehavior alloc] init];
[self.animator addBehavior:_gravity];
}
return _gravity;
}
Add the dropView to the collider in the drop method
-(void)drop
{
CGRect frame;
frame.origin = CGPointZero;
frame.size = DROP_SIZE;
int x = (arc4random() % (int)self.gameView.bounds.size.width) / DROP_SIZE.width;
frame.origin.x = x * DROP_SIZE.width;
UIView *dropView = [[UIView alloc] initWithFrame:frame];
dropView.backgroundColor = self.randomColor;
[self.gameView addSubview:dropView];
[self.gravity addItem:dropView];
[self.collider addItem:dropView];
}
When you instantiate your UIDynamicAnimator, use initWithReferenceView instead of init. Then when you use translatesReferenceBoundsIntoBoundary, it will know what reference bounds to use.
I'm in the same course and thought I was seeing the same issue. However, try running in the 4.0 inch simulator. Your squares are probably collecting just offscreen (outside the bounds of a 3.5 inch screen).
I was wondering what is the best way to draw a single point line?
My goal is to draw this line in a tableViewCell to make it look just like the native cell separator.
I don't want to use the native separator because i want to make in a different color and in a different position (not the bottom..).
At first i was using a 1px UIView and colored it in grey. But in Retina displays it looks like 2px.
Also tried using this method:
- (void)drawLine:(CGPoint)startPoint endPoint:(CGPoint)endPoint inColor:(UIColor *)color {
CGMutablePathRef straightLinePath = CGPathCreateMutable();
CGPathMoveToPoint(straightLinePath, NULL, startPoint.x, startPoint.y);
CGPathAddLineToPoint(straightLinePath, NULL, endPoint.x, endPoint.y);
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = straightLinePath;
UIColor *fillColor = color;
shapeLayer.fillColor = fillColor.CGColor;
UIColor *strokeColor = color;
shapeLayer.strokeColor = strokeColor.CGColor;
shapeLayer.lineWidth = 0.5f;
shapeLayer.fillRule = kCAFillRuleNonZero;
[self.layer addSublayer:shapeLayer];
}
It works in like 60% of the times for some reason.. Is something wrong with it?
Anyway ,i'd be happy to hear about a better way.
Thanks.
I did the same with a UIView category. Here are my methods :
#define SEPARATOR_HEIGHT 0.5
- (void)addSeparatorLinesWithColor:(UIColor *)color
{
[self addSeparatorLinesWithColor:color edgeInset:UIEdgeInsetsZero];
}
- (void)addSeparatorLinesWithColor:(UIColor *)color edgeInset:(UIEdgeInsets)edgeInset
{
UIView *topSeparatorView = [[UIView alloc] initWithFrame:CGRectMake(edgeInset.left, - SEPARATOR_HEIGHT, self.frame.size.width - edgeInset.left - edgeInset.right, SEPARATOR_HEIGHT)];
[topSeparatorView setBackgroundColor:color];
[self addSubview:topSeparatorView];
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(edgeInset.left, self.frame.size.height + SEPARATOR_HEIGHT, self.frame.size.width - edgeInset.left - edgeInset.right, SEPARATOR_HEIGHT)];
[separatorView setBackgroundColor:color];
[self addSubview:separatorView];
}
Just to add to Rémy's great answer, it's perhaps even simpler to do this. Make a class UILine.m
#interface UILine:UIView
#end
#implementation UILine
-(id)awakeFromNib
{
// careful, contentScaleFactor does NOT WORK in storyboard during initWithCoder.
// example, float sortaPixel = 1.0/self.contentScaleFactor ... does not work.
// instead, use mainScreen scale which works perfectly:
float sortaPixel = 1.0/[UIScreen mainScreen].scale;
UIView *topSeparatorView = [[UIView alloc] initWithFrame:
CGRectMake(0, 0, self.frame.size.width, sortaPixel)];
topSeparatorView.userInteractionEnabled = NO;
[topSeparatorView setBackgroundColor:self.backgroundColor];
[self addSubview:topSeparatorView];
self.backgroundColor = [UIColor clearColor];
self.userInteractionEnabled = NO;
}
#end
In IB, drop in a UIView, click identity inspector and rename the class to a UILine. Set the width you want in IB. Set the height to 1 or 2 pixels - simply so you can see it in IB. Set the background colour you want in IB. When you run the app it will become a 1-pixel line, that width, in that colour. (You probably should not be affected by any default autoresize settings in storyboard/xib, I couldn't make it break.) You're done.
Note: you may think "Why not just resize the UIView in code in awakeFromNib?" Resizing views upon loading, in a storyboard app, is problematic - see the many questions here about it!
Interesting gotchya: it's likely you'll just make the UIView, say, 10 or 20 pixels high on the storyboard, simply so you can see it. Of course it disappears in the app and you get the pretty one pixel line. But! be sure to remember self.userInteractionEnabled = NO, or it might get over your other, say, buttons!
2016 solution ! https://stackoverflow.com/a/34766567/294884
shapeLayer.lineWidth = 0.5f;
That's a common mistake and is the reason this is working only some of the time. Sometimes this will overlap pixels on the screen exactly and sometimes it won't. The way to draw a single-point line that always works is to draw a one-point-thick rectangle on integer boundaries, and fill it. That way, it will always match the pixels on the screen exactly.
To convert from points to pixels, if you want to do that, use the view's scale factor.
Thus, this will always be one pixel wide:
CGContextFillRect(con, CGRectMake(0,0,desiredLength,1.0/self.contentScaleFactor));
Here's a screen shot showing the line used as a separator, drawn at the top of each cell:
The table view itself has no separators (as is shown by the white space below the three existing cells). I may not be drawing the line in the position, length, and color that you want, but that's your concern, not mine.
AutoLayout method:
I use a plain old UIView and set its height constraint to 1 in Interface Builder. Attached it to the bottom via constraints. Interface builder doesn't allow you to set the height constraint to 0.5, but you can do it in code.
Make a connector for the height constraint, then call this:
// Note: This will be 0.5 on retina screens
self.dividerViewHeightConstraint.constant = 1.0/[UIScreen mainScreen].scale
Worked for me.
FWIW I don't think we need to support non-retina screens anymore. However, I am still using the main screen scale to future proof the app.
You have to take into account the scaling due to retina and that you are not referring to on screen pixels. See Core Graphics Points vs. Pixels.
Addition to Rémy Virin's answer, using Swift 3.0
Creating LineSeparator class:
import UIKit
class LineSeparator: UIView {
override func awakeFromNib() {
let sortaPixel: CGFloat = 1.0/UIScreen.main.scale
let topSeparatorView = UIView()
topSeparatorView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: sortaPixel)
topSeparatorView.isUserInteractionEnabled = false
topSeparatorView.backgroundColor = self.backgroundColor
self.addSubview(topSeparatorView)
self.backgroundColor = UIColor.clear
self.isUserInteractionEnabled = false
}
}
I want to horizontally center a number of UIViews (they happen to be circles) in the master UIView. It will end up basically looking like the dots on the standard Page Control.
I have all the code written to create the circle UIViews I just have no idea how to arrange them horizontally and dynamically at run time.
Essentially I need some kind of horizontal container where I can do this
-(void)addCircle{
[self addSubView:[CircleView init]];
}
And it will auto arrange however many children it has in the center.
I get confused with auto-layout as well from time to time but here is a way how you can do it programmatically: (I assume that you add your circle views to a containerView property of your view controller and you do not add any other views to it.)
Add these two properties to your view controller:
#property (nonatomic) CGRect circleViewFrame;
#property (nonatomic) CGFloat delta;
Initiate those properties with the desired values in your view controller's viewDidLoad method:
// the size (frame) of your circle views
self.circleViewFrame = CGRectMake(0, 0, 10, 10);
// the horizontal distance between your circle views
self.delta = 10.0;
Now we add your "automatic addCircle method":
- (void)addCircleView {
UIView *newCircleView = [self createCircleView];
[self.containerView addSubview:newCircleView];
[self alignCircleViews];
}
Of course we need to implement the createCircleView method...
- (UIView*)createCircleView {
// Create your circle view here - I use a simple square view as an example
UIView *circleView = [[UIView alloc] initWithFrame:self.circleViewFrame];
// Set the backgroundColor to some solid color so you can see the view :)
circleView.backgroundColor = [UIColor redColor];
return circleView;
}
... and the alignCircleViews method:
- (void)alignCircleViews {
int numberOfSubviews = [self.containerView.subviews count];
CGFloat totalWidth = (numberOfSubviews * self.circleViewFrame.size.width) + (numberOfSubviews - 1) * self.delta;
CGFloat x = (self.containerView.frame.size.width / 2) - (totalWidth / 2);
for (int i = 0; i < numberOfSubviews; i++) {
UIView *circleView = self.containerView.subviews[i];
circleView.frame = CGRectMake(x,
self.circleViewFrame.origin.y,
self.circleViewFrame.size.width,
self.circleViewFrame.size.height);
x += self.circleViewFrame.size.width + self.delta;
}
}
This is the most important method which will automatically realign all your subviews each time a new circleView is added. The result will look like this:
Simple steps: append circle to container view, resize container view, center align container view
-(void)addToContanerView:(CircleView*)circle{
circle.rect.frame = CGrectMake(containers_end,container_y,no_change,no_change);
[containerView addSubview:circle];
[containerView sizeToFit];
containerView.center = self.view.center;
}
Assumptions:
containers_end & containers_y you can get from CGRectMax function,
for UIView SizeToFit method check here
To take care of rotation use make sure your Autoresizing subviews are set for left, right bottom and top margin.
You can try using this library. I have used it on several of my projects and so far, it worked really well.
https://github.com/davamale/DMHorizontalView
I'm new to iOS and Cocos development.
I currently have a basic app going on in my HelloWorldLayer class. It contains my sprites and touch interaction methods and all is well.
I'm trying to add another "panel" (UIView?) over top of what is currently seen. Eventually this panel will have buttons or other things that will interact with the main canvas.
How can I include another UIView onto the canvas screen? Through my appDelegate, or my HelloWorldLayer?
Thanks
Here is one way to do it. I've used UITextView here but you could use the approach for any descendant of UIView. Bear in mind that UIKit's y coordinate is zero at the top-left of the screen, whereas Cocos2D's is zero at the bottom left.
// Make your subview
UITextView* t = [[UITextView alloc] initWithFrame: CGRectMake(10, 10, 200, 200)];
t.backgroundColor = [UIColor blackColor];
t.textColor = [UIColor whiteColor];
t.text = #"Hello UIKit!";
t.editable = NO;
// Add it as a subview of the Cocos2D view
UIView* cocosView = [[CCDirector sharedDirector] openGLView];
[cocosView addSubview:t];
Alternatively you could try Blue Ether's CCUIViewWrapper, repository here.