Detecting Multitouch Gestures in iOS

Before iOS 3.2 was introduced on the iPad (and latter with iOS 4.0 on the iPhone and iPod Touch), interpreting touch events as different gestures was an awkward task, and relied upon subclassing UIView, and then implementing the following methods.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

If you were trying to detect if a user was dragging a view (or panning as Apple would say), you would usually implement the touchesBegan:withEvent: to start dragging the piece, touchesMoved:withEvent: to adjust the view’s frame, and then touchesEnded:withEvent: and touchesCancelled:withEvent: to stop detecting the drag. While implementing dragging was not too complex, trying to detect a more advance gesture such as a swipe or pinch was a lot more complex … and a lot of hard work to make it replicates Apple’s version of the gestures accurately.

Thankfully Apple created UIGestureRecognizer:

UIGestureRecognizer is an abstract base class for concrete gesture-recognizer classes. A gesture-recognizer object (or, simply, a gesture recognizer) decouples the logic for recognizing a gesture and acting on that recognition. When one of these objects recognizes a common gesture or, in some cases, a change in the gesture, it sends an action message to each designated target object. Taken from Apple

As UIGestureRecognizer is an abstract class, you will never use it directly. You will however use one of its subclasses, that have been designed specifically to capture a specific gesture. objc UITapGestureRecognizer UIPinchGestureRecognizer UIRotationGestureRecognizer UISwipeGestureRecognizer UIPanGestureRecognizer UILongPressGestureRecognizer

So lets take UIPanGestureRecognizer for an example, and we will use it to move a view around.

Our example view controller has a view (obviously), and this has a subview called panView, which is the view we will move around.

First thing that you will need to do, is create a pan gesture recognizer (UIPanGestureRecognizer) and add it to the panView. This will allow you to detect the gesture. To get informed about the gesture getting detected, you have to set the view controller as the delegate, and then have it call the the** panViewWithGestureRecognizer:** selector. (Note: You can set any object as the delegate, as long as it implements UIGestureRecognizerDelegate protocol. For this example the view controller makes sence.)

UIPanGestureRecognizer *panGesture = nil;
panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panViewWithGestureRecognizer:)];

[panGesture setMaximumNumberOfTouches:2];
[panGesture setDelegate:self];

[panView addGestureRecognizer:panGesture];
[panGesture release];

panViewWithGestureRecognizer: will get called when the gesture changes state, the possible states are:

UIGestureRecognizerStatePossible  
UIGestureRecognizerStateBegan    
UIGestureRecognizerStateChanged   
UIGestureRecognizerStateEnded    
UIGestureRecognizerStatePossible
UIGestureRecognizerStateCancelled   
UIGestureRecognizerStateFailed
UIGestureRecognizerStateRecognized
UIGestureRecognizerStateEnded

For this example we only care about if gesture began (UIGestureRecognizerStateBegan) or has changed (UIGestureRecognizerStateChanged). When a pan gesture happens (begins or changes) we want to move the center of panView to be underneath the users finger, thus dragging the piece.

To do this we implement panViewWithGestureRecognizer: in the following way.

- (void)panViewWithGestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer
{    

    if ([gestureRecognizer state] == UIGestureRecognizerStateBegan 
 || [gestureRecognizer state] == UIGestureRecognizerStateChanged) 
    {

CGPoint translation = [gestureRecognizer translationInView:[panView superview]];

[piece setCenter:CGPointMake([panView center].x + translation.x, [panView center].y + translation.y)];
[gestureRecognizer setTranslation:CGPointZero inView:[panView superview]];
    }
}

What makes this super easy is the convenience methodtranslationInView:, which is found on UIPanGestureRecognizer. This gives you the value of where the center of the panView should be, without having to calculate it yourself. We then need to set the translation back to zero using setTranslation:, so when we calculate the next gesture, it will be from the new position of the panView. And thats all there is to it.

As mentioned before there are a few gesture recogniser that have already been prebuilt for you to use, and you can even add multiple gesture recogniser to the same view. If you do this, you will want to implement:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

This simply allows you to specify if a gesture should be detected alongside another gesture e.g. a pinch and a rotation gesture. If Apple’s pre-built gesture recognizers are not enough for you, you can always create your own subclass of UIGestureRecognizer.

"Back to the Mac" - Thoughts and Predictions

Apple are looking to close off the year (in terms of keynotes anyway), with Steve Jobs talking about the Mac. The 4 keynotes previously in the year, focused on the iPad, iPhone and more recently the iPod line up.

If pictures paint a thousand words, the invitation clearly shows a lion, so one would assume that this event will be the unvaling of Mac OS 10.7 Lion.

As I like guessing a bit too much, here are my predictions for what we will see in Mac OS 10.7.

What I think will be in it:

What might be in it:

What needs to happen but won’t:

Other than Mac OS X, there is more than likely going to be a Mac hardware announcement along side iLife/iWork 2011. The MacBook Air seems to be the safe bet, but I also expect the entire MBP line to get a speed bump … Can I wish for USB 3.0 ?

One more thing … Maybe we get to find out what the “revolutionary feature” is.

Core Animation using blocks

Previously I have written a blog post about doing simple animations with UIViews, which you can find here. Starting in iOS 4.0 you can now do this with blocks.

Blocks have two major benefits (although there are more):

The most common method that you will use to animate views using blocks, is the following class method on UIView:

+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;

This method allows you to set a time interval, along with an animations block and a completion block.

Lets say we have a UIView variable called redView.

redView = [[UIView alloc] initWithFrame:CGRectMake(0,0,320,320)];
redView.backgroundColor = [UIColor redColor];

We will then add redView as a subview of the view controller’s view

[self.view addSubview:redView];

A common situation you may find your self in, is when you finish animating a view (e.g. changing its alpha), you would then want to remove it from it’s superview, and then release it from memory.

So if you had (for example) a method called animate to trigger off the animation, you would need to add an “animation did stop selector” to the animation. In the following example I have called it cleanUp.

- (void)animate{
   [UIView beginAnimations:@"" context:NULL];
   [UIView setAnimationDuration:5.0];
   [UIView setAnimationDidStopSelector:@selector(cleanUp)];
   [redView setAlpha:0.0];
   [UIView commitAnimations];

}

- (void)cleanUp{
    [redView removeFromSuperview];
    // I do release and = nil on the same line as a coding convention
    // so I don't forget to "nil" the variable
    [redView release], redView = nil;   
}

Although this works well enough, it does mean that your code is split up into the animation code and the (completion) clean up code. If you end up doing this a lot, your code can end up becoming very fragmented and hard to follow.

Using blocks we can do the following:

- (void)animate{

[UIView animateWithDuration:5.0

                    animations:^{ 
                              redView.alpha = 0.0; 
                         }

                   completion:^(BOOL  completed){
                              [redView removeFromSuperview];
                          [redView release],redView = nil;
                        }
 ];


}

This keeps things nice and simple, and it means that you can easily see what code will be executed when the animation completes.

All of the animations you wish to do are passed in using a block:

^{ 
redView.alpha = 0.0; 
}

This block takes no arguments, and it also does not have a return type (so it defaults to void).

Instead of calling the cleanUp selector, we can simply pass in a completion block:

^(BOOL  completed){
[redView removeFromSuperview];
[redView release],redView = nil;
}

This block takes one parameter, which is a bool signifying if the animation has completed when the block is executed. In this example we ignore the completed variable, and we assume that the animation has completed for simplicity reasons. In the body of the block we do same as we did in the cleanUp selector, we remove redView from its super view and then release it from memory.

In some situations you won’t need to use blocks for Core Animation, but when you do, you will find it cleans up your code no end.

Apple's Master Plan

There is a theme coming from Cupertino, which was made slightly more apparent yesterday when they removed storage from the Apple TV … the master plan.

Apple are clearly going for the “your family has one master computer, and you sync lots of (smaller/lower cost) devices with it” distribution model … all of these devices are available from Apple. iPads are replacing laptops (for some), and people have long bought into the iPod revolution to play there digital media on the move. The AppleTV is a bit different as you can stream rented content from Apple to it, but the only way to get your own content on it, is to stream it from another device. This would ether be from your computer, or an iOS device which you have already synced with your computer.

Although “the cloud” is clearly the future, it isn’t yet ready for prime time. Until everyone in their normal every day lives has access to a (constant) high speed internet connection, the cloud is merely a pipe dream in terms of using it for all of your data driven tasks.

Don’t get me wrong, the clouds time will come, but in the next 5 years ? … I’m not so sure.

At the moment client applications (native applications that sync with a server and then cache the data for offline use), are the correct implementation for the the moment. As far as streaming movies go, Apple knows that all AppleTVs will live in a home, and therefore (hopefully) have the bandwidth required to play HD footage.

Although people will disagree, the master plan is probably the right choice for the start of this decade, but the cloud’s time will come.

Rounding the corners of an UIView

Rounded corners are all the rage at the moment, so you would think that there would be an API on UIView to do such a thing. If you looked at all of the UIView documentation, you would come to the conclusion that there is not an API to do such a thing, and you would be (kind of) wrong.

Although UIView does not allow you round its corners, CALayer does, and every UIView is backed by a CALayer.

To access all of the functionality of CALayer, you will need to import QuartzCore into any of header/implementation files that you intend to use CALayer in.

#import <QuartzCore/QuartzCore.h>

CALayer has a lot of APIs available to it, but the property we are interested in is:

@property CGFloat cornerRadius

As the property suggests, this allows you to set the CALayer‘s corner radius in points:

UIView *view = [[UIView alloc] initWithFrame:frame];
view.layer.cornerRadius = 10.0;

CALayer also has properties allowing you to set its border, shadow etc, so all of these can save you a lot of code … and unwanted images.

Retrieving the currently being played music track

One question I keep hearing is “Can you find out what music track a user is listening to? as I want to use it for …”, where the reason usually revolves around posting it to a social network network site, or using it as IM status. Thankfully retrieving the currently being played music track is very easy thanks to the MediaPlayer Framework.

Each application has its own MPMusicPlayerController, but it also has access to the iPod’s MPMusicPlayerController, using the class method iPodMusicPlayer.

MPMusicPlayerController *iPodMusicPlayerController = [MPMusicPlayerController iPodMusicPlayer];

After you have got the iPod music player, you can then get the now playing item

MPMediaItem *nowPlayingItem = [iPodMusicPlayerController nowPlayingItem];

If the now playing item is nil, you know that the user is not playing a music track on their iPod.

Unlike many of the other APIs in iOS, you can’t access information such as the track name, via a simple string property. You have to use one of the following keys:

NSString *const MPMediaItemPropertyPersistentID;
NSString *const MPMediaItemPropertyMediaType; 
NSString *const MPMediaItemPropertyTitle;
NSString *const MPMediaItemPropertyAlbumTitle; 
NSString *const MPMediaItemPropertyArtist; 
NSString *const MPMediaItemPropertyAlbumArtist; 
NSString *const MPMediaItemPropertyGenre;
NSString *const MPMediaItemPropertyComposer; 
NSString *const MPMediaItemPropertyPlaybackDuration;
NSString *const MPMediaItemPropertyAlbumTrackNumber;
NSString *const MPMediaItemPropertyAlbumTrackCount;
NSString *const MPMediaItemPropertyDiscNumber;
NSString *const MPMediaItemPropertyDiscCount;
NSString *const MPMediaItemPropertyArtwork;
NSString *const MPMediaItemPropertyLyrics;
NSString *const MPMediaItemPropertyIsCompilation;  
NSString *const MPMediaItemPropertyReleaseDate;
NSString *const MPMediaItemPropertyBeatsPerMinute;
NSString *const MPMediaItemPropertyComments;
NSString *const MPMediaItemPropertyAssetURL;

And then query the Media Player Item, using the instance method valueForProperty:.

NSString *itemTitle = [nowPlayingItem valueForProperty:MPMediaItemPropertyTitle];

And thus you will end up with a code snippet like this:

MPMusicPlayerController *iPodMusicPlayerController = [MPMusicPlayerController iPodMusicPlayer];

MPMediaItem *nowPlayingItem = [iPodMusicPlayerController nowPlayingItem];   

if (nowPlayingItem) {
    NSString *itemTitle = [nowPlayingItem valueForProperty:MPMediaItemPropertyTitle];
    NSLog(@"User is playing the following song: %@",itemTitle);
} else {
    NSLog(@"User is not playing a song");
}

As always this is just a small code snippet to get you started. There are situations for instance, where the user can have a now playing item that has no title (strange I know). So as always you will have to handle these edge cases appropriately.

WWDC 2010 Keynote Predictions

I thought this year I would put them down in writing, so here is my list:

What I want to see, but I don’t think we will … a preview of Mac OS 10.7. My dream is for Steve to say that 10.7 will have UIKit available to it, and I can stop using NSOutlineView, NSCell to name but a few.

MapKit

Location, Location, Location is all the rage at the moment. Where did someone last update their social network status ? Where is the nearest restaurant ? Where do I need to be in 5 mins ? the possibilities for location services are (nearly) endless. But what is a location ? well put simple it is just a co-ordinate value (longitude and latitude), and in the case of iPhone, it is in reference to the earth. But lets be honest, displaying -122.03, 37.33 in your sexy new iPhone application is not very exciting now is it ? That is were MKMapView comes in.

MKMapView allows you to display a Map on your iPhone’s screen. MKMapView is just a subclass of UIView so you can treat it like one. You can ether use it as a subview (such as on a profile page), or make it go full screen (using it with its own view controller) such as when you are viewing a map to get directions.

So like UIView, to create a MKMapView you need to initialise one with a frame:

MKMapView *mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0,0,320,480)];

Unlike UIView, you will also want to become the delegate of MKMapView.

mapView.delegate = self;

The delegate methods for MKMapView include ones that inform you about when the map starts and finishes to load, in addition to if it fails to load at all. (These delegate methods are very similar to UIWebView if you have used that).

- (void)mapViewWillStartLoadingMap:(MKMapView *)mapView;
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView;
- (void)mapViewDidFailLoadingMap:(MKMapView *)mapView withError:(NSError *)error;

In addition to the loading of the Map, the delegate also provides call backs for when the map’s region changes, it also provides callbacks regarding annotation information (more on that later).

- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated;
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated;

After you have created a MKMapView, you will want to show a specific region on the map. The method to do this is:

- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated;

This introduces a new MapKit data type MKCoordinateRegion. This is the centre point (that you want to show) expressed as a co-ordinate, and then the distance (span) around this co-ordinate that you want to be shown. In essence the span is the zoom level. The bigger the span, the bigger the area that is displayed on the map.

To make a new region you will need to use:

MKCoordinateRegionMake(CLLocationCoordinate2D centerCoordinate, MKCoordinateSpan span);

And thus you will probably have some code that looks like this:

CLLocationCoordinate2D coordinate;
coordinate.latitude = -122.03;
coordinate.longitude = 37.33;

MKCoordinateSpan span = MKCoordinateSpanMake(0.003, 0.003);

[mapView setRegion:MKCoordinateRegionMake(coordinate, MKCoordinateSpanMake(0.003, 0.003)) animated:YES];

After you have focused in on a specific region of the map, you will more than likely want to annotate a given point. To do this you need to create an object that conforms to the (informal) MKAnnotation protocol, and in particular implements the CLLocationCoordinate2D coordinate property.

e.g.

@interface MCSMMapAnnotation : NSObject {

}

@property (nonatomic) CLLocationCoordinate2D coordinate;

@end

Then you just need to create an instance of this model object, and then add it to the map as an annotation:

MCSMMapAnnotation *annotation = [[MCSMMapAnnotation alloc] init];

CLLocationCoordinate2D annotationCoordinate;
coordinate.latitude = -122.03;
coordinate.longitude = 37.33;
annotation.coordinate = annotationCoordinate;
[self.mapView addAnnotation:annotation];

And that is it, nice and simple.

One more thing …

Before I mentioned that there are delegate methods regarding the annotations on the Map View. One that is particular useful is:

- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)annotationViews;

Why is it useful ? Well if you didn’t notice, doing the above means that the annotation(s) just appear on the screen and they do not drop on. So in this delegate method, you can move the annotations off screen and then put them back to where MapKit said they should be.

- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)annotationViews {

    // The final (correct) position of the annotation
    CGRect finalFrame;

    // The position we will drop the annotation from
    CGRect offScreenFrame;      

    for(UIView *annotationView in annotationViews) {
        // MapKit has worked out the annotations final position so store it
        finalFrame = annotationView.frame;

        // We just want to move the annotation, so it is just above the top of the visible screen

        offScreenFrame = CGRectMake(finalFrame.origin.x,(finalFrame.size.height *-1) , finalFrame.size.width, finalFrame.size.height);

        // Set and therefore move the annotation to the off screen position
        annotationView.frame = offScreenFrame;

        // Set up an animation block to animate the drop
        [UIView beginAnimations:@"AnimateAnnotation" context:NULL];
        [UIView setAnimationDuration:1.0];

        // Set the final frame, to be the frame that map kit originally calculated
        annotationView.frame = finalFrame;
        [UIView commitAnimations];
    }       
}

This is just a brief overview of what you can do with MapKit. The most obvious thing you can do, is to add more than one annotation to the map, and the above animation trick is designed to work with this. One feature you may want to do if your application heavily relies upon maps, is to create your own custom pins (that don’t even need to look like pins), to point out specific points on the map.

iPad - The sweet spot in software development

Now that the iPad (iPhone OS 3.2) SDK is no longer under NDA (I think anyway), I thought I would post my opinion on developing for it.

I was programming in Objective-C before the iPhone was even announced (only for a around a year and a half, but I shall still claim it), and switching to the iPhone OS was somewhat strange. At first all I could see is what was missing, all those APIs I took for granted, and things like bindings that I had come to love and (overly) rely on.

In retrospect these limitations where a good thing. Learning how to do things the long way round (such as UIs in code) meant you actually got to know how they worked, and it wasn’t just because interface builder was magic. I still to this day write a lot of my iPhone UIs purely in code. Why you ask ? If your UI is complex then sometimes interface builder just doesn’t do what you need. This is especially the case on the iPhone, as you are often animating views on and off the screen, making them bigger and smaller etc etc. Also having half of your logic in a xib, and half of it in source code can lead to its own problems. I hope that I am not the only one who has wasted significant amount of time after forgetting to hook up an IBOutlet or IBAction (the one time you don’t want messages that are sent to nil, to be ignored !!!).

After spending about a year exclusively programming for the iPhone OS in my day job, I got an urge to write a little application for Mac OS X. And in a strange turn of events I had the same feelings when I moved back to the mac, as when I moved to the iPhone OS. You can’t animate an NSView with the same level of ease as you can a UIView, and some of the Mac OSX APIs are not very clean at all (try playing with NSOutlineView to name one class). This is probably expected as some of these APIs have been around for over a decade, but the biggest surprise is what you forgot you had to do when creating Mac applications. Keyboard shortcuts, window resizing and providing automatic updates of your applications to name but a few. Apple ether take care of this for you on the iPhone, or they are just not a feature of the OS. Having the limitations of a fixed size screen, and only being able to get applications from the AppStore doesn’t seem such a bad deal after all.

Then came the iPad, its like a Big iPhone from a programmers point view. Its the iPhone OS with a couple of extras, but with a really big display. The jewel in the iPhone OS’s crown is its UI Libraries. It is so simple (compared to any other platform, mobile or otherwise), to build really slick animated interfaces that your users will love to use. Now that you have all this screen to play with, you can start building “Desktop Class” applications but with the iPhones APIs. The thing that has shocked me the most, is the quality of all of the iPad applications that have been released. The vast majority of developers have all gone the extra mile to make their UIs, and therefore their applications look extremely polished. This brings around the great debate of application pricing. Everyone with any business acumen would have long worked out that £0.59/$0.99 is not a sustainable business model for 99% of developers, and the iPad has seemed to have broken this trend from day one. Applications seem to be in the range of £3/$5 - £10/$20, which will probably lead to less applications being sold, but in the long term will lead to a higher quality applications that can be supported by their respective developers.

The one thing I am hoping for is that the iPhone APIs get ported back to Mac OS X, or maybe that iPhone OS becomes the Mac OS. Saying that, no multitasking and the AppStore being the sole way of getting apps onto my mac is a scary thought…

MacRuby - First Impression

What not Objective-C !!! whats going on, I hear you cry. Well I thought I would mix things up a bit, after all it has got Mac in the name.

I am a firm believer that learning a new language, makes you better at developing with the ones you already know. Different languages often attack problems from different angles, depending on the language’s capabilities (built in classes etc) and common design patterns. Im sure people that are new to iPhone/Mac programming, where shocked about how often they had to type the word delegate.

So why Ruby?

I admit it, I have played with Ruby before and the fact that I quite like it did help its cause, but that isn’t the main reason. The main reason is the fact you can use it for almost anything. You can use it as a CGI script on the web, you can build Web applications using Ruby On Rails, you can do shell scripting to automate tasks, and obviously you can build desktop applications.

So what is MacRuby?

MacRuby is Ruby 1.9 built on top of the Objective-C runtime, which allows you to build Mac applications using Ruby (Its actually an Apple run project). The real sexy bit is you can call any Objective-C method directly using Ruby, meaning that you have access to all of OS X functionality. What is even more surprising, is that you can use Interface builder to put together your UI (well it was a surprise to me anyway).

Unfortunately MacRuby doesn’t currently work on the iPhone as it needs the garbage collector, but at some point this may change.

Interface Builder

So if your using interface builder, your need to declare IBOutlets so you can access a UI elements from in your code.

IBOutlet NSTableView *tableView;

In Ruby you don’t actually type cast your variables, so the only way to achieve this is to declare an accessor to that variable.

attr_accessor :tableView

In addition to IBOutlets your also need to declare IBActions for button presses etc.

- (IBAction)pressMe:(id)sender {

}

In Ruby you need to define a method with the sender parameter

def pressedButton(sender)
end

Calling Objective-C methods from Ruby

As you are building Mac application with Ruby, you will want to access the vast array of APIs available in Mac OS X. These are (mostly) written in Objective-C. One common task is creating a string:

NSString *string = [NSString stringWithString:@"Test string"];

In Ruby you do the following. Notice that there is no pointers and you do not need to declare a type.

string = NSString.alloc.initWithString("Test String")

*As Ted Wise correctly points out in the comments, you can create a NSMutableString by do the following piece of Ruby code, but I thought the string example was a nice and concise one to use.

string = "Test String"

From the above code snippets you will also notice that like most languages, Ruby doesn’t “stagger” its variables like Objective-C.

So how would you call the following Objective-C code snippet in Ruby ?

NSAlert *alert = [NSAlert alertWithMessageText:@"title"
defaultButton:@"ok"
alternateButton:nil
otherButton:nil
informativeTextWithFormat:@"This is an alertview"];
[alert runModal];    

Well quite easily actually, as MacRuby actually adds this syntax to Ruby, so it would look like the following:

alert = NSAlert.alertWithMessageText("Title", defaultButton:"OK", alternateButton:nil, otherButton:nil, informativeTextWithFormat:"This is an alertview")

alert.runModal()

Easy enough ?

Conclusion (of my first impressions)

This post quite literally touches the surface of MacRuby, but I think it is an exciting technology. Ruby is really good for getting stuff done quickly, as the language does a lot of heavy lifting for you. By doing the Model part of the MVC in Ruby, you could in theory share the code across applications on the desktop and the web, which can’t be bad.

If your new to the Mac and don’t know ether Objective-C or Ruby, I would still learn Objective-C first if I am being honest. Learning Cocoa is hard enough without all the examples being in another language. But if your new to the Mac and already know Ruby, it might be a good way to start mac development.

Page 7 of 9