What Haven't You Tried?
Matt Gemmell wrote a fantastic essay on a disturbing phenomenon that seems to plague software development far more than most professional fields: Too many people just aren't willing to figure things out on their own. Consequently, our field is full of "developers" who are really nothing more than Professional Copy-Pasters.
This strongly matches my experiences in University as well. Most Computer Science professors are well aware of the problem and, as such, try to force their students to learn the necessary problem-solving skills, usually by assigning projects which deal with material or concepts which haven't been directly-addressed in class. The end result, however, is that students encounter something which wasn't taught in class and immediately run to a TA for clarification. Rinse and repeat this cycle over the entire course, and you wind up with a huge percentage of students asking for help every time they encounter something they aren't immediately proficient with.
This is especially evident the day before a large assignment is due. Walking by a TA's office, there's always a line of students waiting to ask last-minute implementation questions. So, while you have maybe 75% of the students asking things like "How do I use function X?" (a question whose answer is quickly found by a simple Google search), there's a small group of students in the back of the line waiting to ask a legitimate question, which they've likely already wasted hours or days trying to figure out. And, since a TA will rarely tell a student to leave, the line gets longer and longer, and the students with legitimate questions waste even more time.
The end result is that many CS students never learn to figure things out on their own. After all, why try to debug that crash if the TA will do it for you? Why learn the ins-and-outs of Algorithm X if the TA will basically give you pseudocode. Expecting someone else to do things for you is no longer an ailment which plagues the informally-taught. The attitude prevails in more universities than anyone would think, due in large part to the babying nature of today's Computer Science programs. Of course, there are plenty of us who make it through with a deeper understanding of systems and languages, complete with the necessary skills to expand our knowledge independently. But, it is my experience that, in the general case, the system fails far too often -- It takes a certain amount of internal drive to avoid falling victim to the siren song of "do-it-for-me-itis," and with today's students being drawn to the field by the allure of fame and riches rather than by an interest and appreciation for the subject matter, students seem to be missing this drive more and more with each new class.
That isn't to say that University CS programs are a waste of time -- There are several subjects which I would have never been interested in, much less learned, on my own. But there is something fundamentally broken about the way software engineering is taught. The system is letting far too many students coast by without learning the necessary skills, and the future of the software industry will feel the effects.
0 Comments
iPhone RSS Reader Icon
Quick post today!
I've been working on an iPhone icon for an RSS reader over the last few days. Here it is, to do with as you please! (I'm using it for NetNewsWire)
4 Comments
Building a Preferences Window
The preferences window is a key part of most Cocoa applications. There are a lot of different ways to create them, and I want to briefly walk through my way of doing things. Before getting started, it's worth noting that my general design mirrors that of Apple's private NSPreferences class. However, I've eliminated a lot of the verbosity and redundancy (as well as the unapproved-ness) of NSPreferences. With that out of the way, let's get started!
The Basics
Every preferences window has a few basic things in common. There's a window with a toolbar, which is used to move between different panes. In-keeping with the NSPreferences design, I've called these panes "modules." Each module has a title, an icon, a unique identifier, and a view, which will be displayed when the user clicks on the module's toolbar icon.
So, now that we've got a list of requirements for each module, let's formalize these requirements, using a formal protocol:
// All modules must conform to this protocol
@protocol MBPreferencesModule
@required
- (NSString *)title;
- (NSString *)identifier;
- (NSImage *)image;
- (NSView *)view;
@optional
- (void)willBeDisplayed;
@end
You'll notice that I've also added an optional -willBeDisplayed method -- I've done this to allow for lazy loading of preferences. Basically, if a module does all of its initial data loading inside of a -willBeDisplayed method, we can delay all of these loads until the module is activated, speeding up load time of the window itself. The difference may not be noticeable for most applications (which is why I've made the method optional), but for a module that needs to initialize a physical device or perform complex calculations, it can make a world of difference.
The Controller
Now know what methods our modules will implement. Great! But we still need to have an actual window and handle all the module switching. For this, let's create a subclass of NSWindowController:
@interface MBPreferencesController : NSWindowController {
NSArray *_modules;
id<MBPreferencesModule> _currentModule;
}
// We only want one preference window per application
+ (MBPreferencesController *)sharedController;
// All of the modules to place in the window
@property(retain) NSArray *modules;
// Convenience method
- (id<MBPreferencesModule>)moduleForIdentifier:(NSString *)identifier;
@end
Pretty basic, no? I've kept the public interface for this class very basic, since there shouldn't be much outside interaction involved. Let's start implementing our class!
To begin with, we need to set up all of our initialization code. Since we only want one shared instance (accessed using the +sharedController class method), I've pulled the majority of the code from Apple's singlton example. We do, however, need to deal with property synthesis and memory management:
@synthesize modules=_modules;
- (void)dealloc
{
self.modules = nil;
[super dealloc];
}
Now, we still don't have a window, do we? We need a window that has the standard appearance for a preference window (closable only, toolbar, hidden toolbar button, etc). Let's take care of that during initialization:
- (id)init
{
if (self = [super init]) {
// The initial size of the window doesn't matter
// We'll be setting the window size later
NSWindow *prefsWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 300, 200) styleMask:(NSTitledWindowMask | NSClosableWindowMask) backing:NSBackingStoreBuffered defer:YES];
[prefsWindow setShowsToolbarButton:NO];
self.window = prefsWindow;
[prefsWindow release];
// Create the toolbar. This is handled below.
[self _setupToolbar];
}
return self;
}
- (void)_setupToolbar
{
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"PreferencesToolbar"];
[toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
[toolbar setAllowsUserCustomization:NO];
[toolbar setDelegate:self];
[toolbar setAutosavesConfiguration:NO];
[self.window setToolbar:toolbar];
}
Now we just have a little more work to get our window in working order. NSToolbar requires its delegate (that's us!) to implement a few methods so it knows what items to display. Let's implement the standard ones:
- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
{
// Each module has a toolbar item
NSMutableArray *identifiers = [NSMutableArray array];
for (id<MBPreferencesModule> module in self.modules) {
[identifiers addObject:[module identifier]];
}
return identifiers;
}
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
{
// We start off with no items.
// We'll add them when we set the modules
return nil;
}
- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
{
// Every toolbar icon is selectable
return [self toolbarAllowedItemIdentifiers:toolbar];
}
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
{
id<MBPreferencesModule> module = [self moduleForIdentifier:itemIdentifier];
NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
if (!module)
return [item autorelease];
// Set the attributes of the item
[item setLabel:[module title]];
[item setImage:[module image]];
[item setTarget:self];
[item setAction:@selector(_selectModule:)];
return [item autorelease];
}
Module Switching
This gives us a window with a toolbar, but right now it doesn't do anything. We need to deal with switching between the views. But before we can do that, we need to implement the convenience method from the interface:
- (id<MBPreferencesModule>)moduleForIdentifier:(NSString *)identifier
{
// Compare identifier against each module's identifier
for (id<MBPreferencesModule> module in self.modules) {
if ([[module identifier] isEqualToString:identifier]) {
return module;
}
}
return nil;
}
That's pretty self-explanatory, and it leaves us free to implement our view switching methods. I've broken this into two methods: -_selectModule:, which will be called whenever a toolbar item is clicked, and -_changeToModule:, which handles the actual view switching. This allows us to change the view programmatically as well, which will come into play in a bit.
- (void)_selectModule:(NSToolbarItem *)sender
{
// This action should only be called by toolbar items
if (![sender isKindOfClass:[NSToolbarItem class]])
return;
// Find the module which is represented by the item
id<MBPreferencesModule> module = [self moduleForIdentifier:[sender itemIdentifier]];
if (!module)
return;
[self _changeToModule:module];
}
- (void)_changeToModule:(id<MBPreferencesModule>)module
{
[[_currentModule view] removeFromSuperview];
// The view which will be displayed
NSView *newView = [module view];
// Resize the window
// Be sure to keep the top-left corner stationary
NSRect newWindowFrame = [self.window frameRectForContentRect:[newView frame]];
newWindowFrame.origin = [self.window frame].origin;
newWindowFrame.origin.y -= newWindowFrame.size.height - [self.window frame].size.height;
[self.window setFrame:newWindowFrame display:YES animate:YES];
[[self.window toolbar] setSelectedItemIdentifier:[module identifier]];
[self.window setTitle:[module title]];
// Call the optional protocol method if the module implements it
if ([(NSObject *)module respondsToSelector:@selector(willBeDisplayed)]) {
[module willBeDisplayed];
}
// Show the view
_currentModule = module;
[[self.window contentView] addSubview:[_currentModule view]];
// Autosave the selection
[[NSUserDefaults standardUserDefaults] setObject:[module identifier] forKey:@"MBPreferencesSelection"];
}
Looks good. The only thing that sticks out is this line:
// Autosave the selection
[[NSUserDefaults standardUserDefaults] setObject:[module identifier] forKey:@"MBPreferencesSelection"];
Something I haven't mentioned until now is the fact that standard preference windows remember their selection, even across launches. We need to do the same, so this line makes sure that we always store what the most recently-activated module was. We'll look at restoring this state in a moment.
The Toolbar, Revisited
If you were to compile your app and open the window, you'd notice something striking: The toolbar is empty. Well, the reason for this is simple. Remember this?
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
{
// We start off with no items.
// Add them when we set the modules
return nil;
}
Ah, yes. We never added any items to the toolbar. Since we don't know anything about the modules when we create our toolbar, we need to deal with that later. Like, say, when we set the modules? Let's override the accessor for the property!
- (void)setModules:(NSArray *)newModules
{
if (newModules == _modules)
return;
if (_modules) {
[_modules release];
_modules = nil;
}
if (!newModules)
return;
_modules = [newModules retain];
// Reset the toolbar items
NSToolbar *toolbar = [self.window toolbar];
if (toolbar) {
NSInteger index = [[toolbar items] count]-1;
while (index > 0) {
[toolbar removeItemAtIndex:index];
index--;
}
// Add the new items
for (id<MBPreferencesModule> module in self.modules) {
[toolbar insertItemWithItemIdentifier:[module identifier] atIndex:[[toolbar items] count]];
}
}
// Change to the correct module
// This is where we restore the autosaved info
if ([self.modules count]) {
id<MBPreferencesModule> defaultModule = nil;
// Check the autosave info
NSString *savedIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:MBPreferencesSelectionAutosaveKey];
defaultModule = [self moduleForIdentifier:savedIdentifier];
if (!defaultModule) {
defaultModule = [self.modules objectAtIndex:0];
}
[self _changeToModule:defaultModule];
}
}
It's a bit long, but it's all fairly basic. The good news is, we only have one more method to override before we have our preference window in working order! Even better, it's ridiculously short. Preference windows always open centered on the screen. So, we need to override NSWindowController's -showWindow: method:
- (void)showWindow:(id)sender
{
[self.window center];
[super showWindow:sender];
}
That's it! We're done!
Who Cares?
"This is all well and good, but how can I use this in my application?" you ask? Well, good news! It's ridiculously easy. Simply create your modules, making sure they conform to the MBPreferencesModule protocol (I prefer using NSViewController subclasses) and pass them to the controller using [[MBPreferencesController sharedController] setModules:]. That's it! If you want a better idea of how to use this, I've included a sample project with the code below.
5 Comments
iPhone-style Calculator Dashboard Widget
It's been bugging me for a while that the iPhone's calculator is so much nicer than Dashboard's. Where the iPhone's is fairly subtle, muted, and clean, Dashboard's widget has been blaring that disgusting orange color ever since Tiger. So, I fixed it.
Not only that, but I also removed almost all the images, opting instead to draw everything I could in code (WebKit has some very nice CSS3 support). There's also a handy ± button for quick calculations. Click the image to grab it!
8 Comments
The Coming Wave of Crap
Mike Lee's ejection from Tapulous seems to have sparked a massive outcry against iPhone VCs. Talk abounds of a new technology bubble, caused by a set of VC firms which apparently slept through the first bubble. And damn it, they want their chance to mess up an industry, too! But unlike the dot-com burst, this bubble's eventual burst will have a much more localized impact. And that's bad news for us.
When the dot-com boom went bust, it first and foremost affected those directly involved in the (former) industry. Yes, there were economic impacts, but the biggest impact was on former employees of the affected companies -- the loss of Pets.com didn't affect the average consumer in any significant way. Conversely, when this new bubble created by the iPhone pops, it's going to be us, the consumers, who feel it most. Surprisingly, I'm not talking about iPhone owners; I'm talking about Mac owners.
New Recruits
The iPhone has drawn a ridiculous number of people to the Cocoa frameworks -- some of their own desire, and some at the charge of their employer, eager to cash in on the Next Big Thing. Some of these new recruits have done a phenomenal job -- they've truly gone the extra mile to make sure they understand what they're doing. However, as evidenced by the quality of a huge percentage of apps on the App Store, it's clear that a lot of people simply don't understand the platform and have no intention of learning. Regardless of their attention to detail, all of these people have invested a huge amount of time learning the ins-and-outs of Objective-C and the Cocoa frameworks. So what are they going to do when the bubble bursts and iPhone apps stop being the quick path to the good life? Many will start developing for the Mac.
These converts will fall into two categories: First, the people who have "seen the light" of Cocoa and want to continue using it in some form. This group will probably be comprised of the same people who took the extra time and effort to learn what's expected of an iPhone app. This group is absolutely desirable, and I hope that, even if the bubble somehow continues, people who fall into this group will consider giving Mac programming a try.
The Eventual Decline
It's the other group that worries me. This group, rather than "making the jump" due to a love of Cocoa, will see it more as cutting their losses. After all, they invested all this time into learning Objective-C, and supposedly there's money in the computer-thingies too! They might as well get something out of it! These will be the developers who didn't bother to learn what's expected of an iPhone app, content to push out garbage. And they'll bring that same apathy to Mac programming.
"So what? If you don't like it, you don't have to buy it," you say. Well, yes, that's a good point. In fact, without the App Store acting as a funnel for all applications, it will be even easier to ignore the garbage. But, no matter how easy they are to ignore, they will cheapen the platform. One of the big counterpoints to Windows having far more applications available is that the Mac has a smaller collection of apps, but they tend to be of much greater quality; saw what you will about the quality of Windows itself, but the strength of a platform is equally reliant on its 3rd-party developers as it is on itself. Where will the Mac be in 5 years once its been flooded with a stream of crappy applications? I'm not sure I want to find out.
2 Comments
