Here in part three, we are going to do a few small improvements, and add in some gravity. Mid way through part two I realized that I had not set up a way for the ball’s color to get passed through for drawing. I could add another parameter to refresh to pass in a color, but it dawned on me, we might as well just pass in the ball instance itself, which has everything that’s needed.
Also, why not have the ball itself keep track of its rectangle area? It knows where it is and it knows its own radius, so it should calculate the rect based on these. Let’s tackle that first. Add a new method to Ball. First the declaration in Ball.h:
[c]#import
@interface Ball : NSObject {
CGPoint position;
CGPoint velocity;
CGFloat radius;
CGColorRef color;
}
@property CGPoint position;
@property CGPoint velocity;
@property CGFloat radius;
@property CGColorRef color;
– (void)update;
– (CGRect)getRect;
@end[/c]
There’s a getRect method which recturns a CGRect. And the implementation in Ball.m:
[c]- (CGRect)getRect {
return CGRectMake(position.x – radius, position.y – radius, radius * 2.0, radius * 2.0);
}[/c]
Simple enough.
Now let’s change BallView’s refresh method to accept a Ball. And get rid of ballRect in there and just store a Ball. Here’s BallView.h:
[c]#import
#import “Ball.h”
@interface BallView : UIView {
Ball *ball;
}
– (void)refresh:(Ball *)aBall;
@end[/c]
Note that because BallView is now using the Ball class, we need to import that.
And now BallView.m:
[Note – I fixed an earlier error in here in line 30. Should be fine now.]
[c]#import “BallView.h”
@implementation BallView
– (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Initialization code
}
return self;
}
– (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, ball.color);
CGContextSetLineWidth(context, 1.0);
CGContextAddEllipseInRect(context, [ball getRect]);
CGContextStrokePath(context);
}
– (void)dealloc {
[super dealloc];
}
– (void)refresh:(Ball *)aBall {
ball = aBall;
[self setNeedsDisplay];
}
@end[/c]
A few things going on here. Notice that refresh now takes a pointer to Ball and stores it, then calls setNeedsDisplay. Then in the drawRect method, we use the ball’s color in the CGContextSetSTrokeColorWithColor call, and [ball getRect] in CGContextAddEllipseInRect. Ball has everything we need here.
The last change we need is in the view controller’s onTimer method. Instead of creating a rect and passing that to refresh, we just pass the ball.
[c]- (void)onTimer {
[ball update];
[(BallView *)self.view refresh:ball];
}[/c]
Test that and you should still have a bouncing ball, but now you can change the color of the ball when you create it and this will be reflected in what is drawn in the view.
Now let’s add gravity and bounce. In Ball.h, declare them as CGFloats and properties:
[c]@interface Ball : NSObject {
CGPoint position;
CGPoint velocity;
CGFloat radius;
CGColorRef color;
CGFloat bounce;
CGFloat gravity;
}
@property CGPoint position;
@property CGPoint velocity;
@property CGFloat radius;
@property CGColorRef color;
@property CGFloat bounce;
@property CGFloat gravity;
– (void)update;
– (CGRect)getRect;
@end[/c]
And synthesize these in the implementation .m file:
[c]#import “Ball.h”
@implementation Ball
@synthesize position;
@synthesize velocity;
@synthesize radius;
@synthesize color;
@synthesize bounce;
@synthesize gravity;
…[/c]
Then we change the update method of Ball to add gravity to the y velocity and use bounce instead of the hard coded -1.0.
[c]- (void)update {
velocity.y += gravity;
position.x += velocity.x;
position.y += velocity.y;
if(position.x + radius > 320.0) {
position.x = 320.0 – radius;
velocity.x *= bounce;
}
else if(position.x – radius < 0.0) {
position.x = radius;
velocity.x *= bounce;
}
if(position.y + radius > 460.0) {
position.y = 460.0 – radius;
velocity.y *= bounce;
}
else if(position.y – radius < 0.0) {
position.y = radius;
velocity.y *= bounce;
}
}[/c]
If you test this as is, you'll see some problems. One, no gravity. Two, no bouncing. That's because we haven't initialized the bounce or gravity properties. We could require that the user assigns every property when they make a ball, but you might want some default values in there. You can't assign values when they are created like CGFloat bounce = -1.0f; unfortunately. So we do this in an init method, which is close to a constructor.
[Edit - actually, I just found this reference, which indicates that we should have provided the init method from the start, and should have been calling it right from the start. Oh well, better late than never. 🙂 http://developer.apple.com/DOCUMENTATION/Cocoa/Conceptual/ObjectiveC/Articles/chapter_4_section_2.html#//apple_ref/doc/uid/TP30001163-CH22-SW2]
If you just type init in the Ball.m file, you should get a code completion for init Definition. Choose that and it will create an init method for you, with a place to add your initialization code. This is where you set up all your default values, like so:
[c]- (id) init
{
self = [super init];
if (self != nil) {
position = CGPointMake(100.0, 100.0);
velocity = CGPointMake(5.0, 5.0);
radius = 20.0;
color = [UIColor greenColor].CGColor;
bounce = -0.9f;
gravity = 0.5f;
}
return self;
}[/c]
Unfortunately, the init method does not get called by default, so when you allocate the ball, you call it then. This is in the view controller’s viewDidLoad method:
[c]- (void)viewDidLoad {
[super viewDidLoad];
ball = [[Ball alloc] init];
ball.position = CGPointMake(100.0, 100.0);
ball.velocity = CGPointMake(4.0, 3.0);
ball.radius = 20.0;
ball.color = [UIColor blueColor].CGColor;
[NSTimer scheduledTimerWithTimeInterval:1.0 / 30.0 target:self selector:@selector(onTimer) userInfo:nil repeats:YES];
}[/c]
Line 3 shows the change.
Run this and you have a ball that bounces and finally settles to the “floor”. Pretty neat. Note that you can still customize the properties like we are doing for position, velocity, radius and color. You could also leave all those off as long as you call init, and you’ll get a default ball.
In part 4 we will add touch support!
Great reading here, thanks so much. I bought the beginning iphone dev book that you recommended and have been working through it non-stop for the last two days.
It’ll take a long, long time before I’m even half as comfortable with this stuff as I am with AS3 but it’s definitely been a lot of fun so far and I can’t wait to see the next part of this article 🙂
Thanks again!
Hi,
great tutorial! But right now, I’m kinda stuck in the refresh method in the BallView-class (right before you would add the gravity-code). I get an error on this line:
self.ball = aBall;
the error says:
error: request for member ‘ball’ in something not a structure or union.
I was first typing along but when i got this error i copied your code so it’s kinda strange that it still doesn’t work :s
Thanx!
Yes, I changed that line to remove the self. just say
ball = aBall;
and you should be fine.
success! ball with gravity. 🙂
I noticed that I can fill the ball with
CGContextSetFillColorWithColor(context, ball.color);
CGContextFillPath(context);
but i can’t fill AND stroke the path. it just does whatever is called first.
Steve-o, you can get stroke and fill like this…
– (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, ball.color);
CGContextAddEllipseInRect(context, [ball getRect]);
CGContextFillPath(context);
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(context, 3.0);
CGContextAddEllipseInRect(context, [ball getRect]);
CGContextStrokePath(context);
}
Hi Keith,
I noticed you’re writing a list to synthesize properties…
ie.) @synthesize position;
@synthesize velocity;
etc…
I just wanted to let you know that it is possible to synthesize multiple properties by separating them with a comma like this: @synthesize position, velocity, radius, color, bounce, gravity;
These tutorials are great! Thanks so much for posting them!
It sure is.
Hi Keith, as a convention ObjC coders tend to not prefix their accessor with get (but do prefix mutators with set). So a minor code enhancement here would be to turn the getRect method into a property (say rect) and instead of using property/synthesize, you would roll your own accessor to do the calculation there.
Thanks much for the tutorial.
I got struck with following error
*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘*** -[UIView refresh:]: unrecognized selector sent to instance 0x526190’
From Controller code:
– (void)onTimer {
[ball update];
[(BallView *)self.view refresh:ball];
}
————————-
self.view is UIView and casted to BallView; But refresh method not picked up from BallView.
I am missing code where we are initializing BallView? Is it from App Delegate? and how we are doing it?
Thanks in Advance
I have the same problem as Ravi. I get the following message Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘*** -[UIView refresh:]: unrecognized selector sent to instance.
I am sure what I am missing…
looking forward to the fix
S
I am also getting the same problem as Ravi and Seb, i really appreciate if some could help me in this.
Regards
Ram
I get the same problem! What the hell! How do I fix this so it compiles?
I have the same problem as Ravi. I get the following message Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘*** -[UIView refresh:]: unrecognized selector sent to instance.
I am sure what I am missing…
pretty sure I got the code correct but I am getting a crash soon as the app tries to load the view. if I remove the ball = [[Ball alloc] init]; in the GravityTotorialViewController then the app will launch without crashing.
I am using the latest iPhone SKD 3.0. I am wondering if something changes with the way you use “init” in the latest build of the SKD but not sure.
Any thoughts on how I could trouble shoot this guys ?
Thanks again for all you do Keith.
ok so there most be something with usint “init” in 3.0 Here is what I did to get it not to crash.
In Ball.h I added -(id)runit;
in Ball.m I replaced the “init” function that is in the tutorial above and called in -(id)runit
then in my GravityTutorialViewController.m I changed the line that says “ball = [[Ball alloc] init]; …. where “init” put “runit”. Hope that helps anyone that tries this on 3.0 and gets a crash.
I tried Matthew’s solution but that doesn’t seem to work. I’m still getting the same error. Anyone got a better solution to this?
Aman
p.s: thanks kieth, for the awesome tutorials.
Hi. Finding this tutorial very useful although I am stumped with an error that I’m sure I can’t solve due to a lack of understanding.
I get “Incompatible type for argument 1 of ‘refresh'” when calling the refresh method in the ViewControllers OnTimer method: [(BallView *)self.view refresh:ball];
My interpretation is that refresh is expecting a pointer to ball rather than the ball itself although my code seems to be as he tutorial suggests. Any help would be appreciated.
Thanks
Thank you so much for this fun tutorial, kp!
Although I’m a bloody NOOB, I think I found a problem with the code above: don’t you need a minus in front of the four different uses of bounce in the update method?
You had one in front of the original 1 in that place, and without it the ball doesn’t bounce off the floor, and just rolls to the right to get stuck in the bottom right corner…
I’ll bet you already fixed this in a later article, but I noticed that even though you define the ball’s properties in its init method, you re-define them again after you call the init method in viewDidLoad. I deleted the redundant code and it works fine.
Keith
Thanks so much for this. As a new person to the world of Xcode, I’m really enjoying the learning curve and the fact that you have provided such great code.
I’m running the 4.2 simulator and I’m getting the following simple, but frustrating error (Expected ‘)’ before ‘!’ token. I’ve copied the code below. I’ve tried the “runit” versus “init” but still not fix.
I’ve copied the code below to also ensure I placed the code in the correct place within Ball.m:
// lines above ……
@synthesize bounce;
@synthesize gravity;
-(id) init
{
self=[super init];
if (self ! = nil) { (error above)
position = CGPointMake(100.0, 100.0);
velocity = CGPointMake(5.0, 5.0);
radius = 20.0;
color = [UIColor greenColor].CGColor;
bounce = -0.9f;
gravity = -0.5f;
}
return self;
}
– (void)update {
//……. lines below
Any support would be appreciated. Thanks
Shawn
Hey friends I found this tutorial interesting but I really wanted to know that is it possible to do this for more than one ball? I am really looking forward to it so please help me out with it
Did you find the way to add more ball ..