Replacing Flash with Cocoa
My new series of LiveLessons, Cocoa Programming Fundamentals, was released recently. (You can probably see an ad alongside this article, encouraging you to buy them.) The DVD version comes with a Flash application for playing the videos. In this article, we'll look at how to replace that Flash app with a native Cocoa version. Don't worry if you haven't bought the LiveLessonsyou may still find some of the information in this article to be useful.
Why Replace Flash?
In April 2010, Apple posted an open letter from Steve Jobs on its website, containing a set of (rambling and inconsistent) reasons for not supporting Flash on the iPhone. In case you've been in a coma for the last 15 years, Flash is an Adobe product, purchased from Macromedia, for producing multimedia applications and applets. Used via a plug-in for web browsers and to produce native apps, Flash runs on the vast majority of systems.
The Flash design has some advantages and disadvantages. The advantages are obvious: You can write a Flash app once and then deploy it almost anywhereexactly what Java promised in the 1990s. Flash is more than just a language, though; it's a complete virtual machine that includes features such as sound and video playback, animations, text rendering, vector art, and so on.
The downside is that Flash is a complete platform of its own, running on top of the native platform, which means that Flash apps don't look and feel like other apps. This approach is fine for things like games, where a native look and feel can harm the player's immersion, but not ideal for other purposes. Because Flash handles text internally, we can use text rendering to see a concrete example of this problem. Look at how differently Flash and Cocoa render the same word, Cocoa, in Figure 1 (the Flash version) and Figure 2 (the Cocoa version).
Figure 1 Flash text rendering.
Figure 2 Cocoa text rendering.
Cocoa uses sub-pixel anti-aliasing, whereas Flash uses whole-pixel anti-aliasing. This difference makes the Flash version look quite blurry when rendered at the correct size. Subtle differences in the kerning also make the Flash text look wrong to people who are accustomed to how OS X renders text.
Another problem that many people complain about is Flash's video playback performance. The Flash implementation of H.264 video compression is terrible. Adobe claims that the problem occurs because Apple doesn't expose low-level APIs for acceleration. But this reasoning doesn't explain why FFmpeg uses about half as much CPU effort as Flash does when decoding H.264, and doesn't require hardware acceleration.
Apple ships an H.264 decoder/encoder as part of QuickTime on OS X. Any application can use this tool, rather than reinventing the wheel. Adobe claims that this approach isn't feasible, because Flash needs to composite other things on top of the video. This argument also doesn't make sense, because QuickTime can render to an OpenGL texture or a Core Animation layer, either of which can then be composited into a scene by using hardware acceleration. The fact that this technique isn't feasible for Adobe implies that Flash is doing all of the compositing in software, which would help to explain its poor performance.
Few of these issues are specific to Flash, of course. Cross-platform APIs always have some similar drawbacks. The price of running on multiple platforms is being constrained to the lowest common denominator, which is why good cross-platform applications tend to have a portable core and some per-platform additions.
Designing the Cocoa Application
The viewer application on the LiveLessons DVD is quite simple: Its single window displays some introductory text on the left and an outline view on the right. When you select an item from the outline view, the viewer switches to displaying the selected video.
We can create this application very easily in Cocoa. In the nib file, we create a window containing all of the views for the initial state. We also create a free-standing NSView containing the QTMovieView (for playing the videos) and some other controls. Switching between the windows is just a matter of replacing one with the other in the view hierarchy, which we can do with this method:
- (void)switchViews { [self exitFullScreen: self]; NSView *v = [window contentView]; NSView *newView = inVideo ? menuView : videoView; NSView *oldView = inVideo ? videoView : menuView; newView.frame = oldView.frame; [v.animator replaceSubview: oldView with: newView]; inVideo = !inVideo; }
Notice that we use the animator property of the window's content view. This is a proxy object used by Core Animation. When you send a message that changes the target state, the animator proxy animates the transition, so you can control the type and duration of the animation entirely in Interface Builder. You don't need to modify the code at all to use a different animationjust select the desired animation from the inspector in Interface Builder.
Because this is a very simple application, we'll create only one new class, which we'll set as the application's delegate. This entire program is only a couple of hundred lines of code. We could separate it out a bit more, but it's not really worth the effort. If you want to extend it to do more complicated things, some refactoring might be in order.
The application shouldn't hardcode anything about the lesson itself. The Flash version uses an XML file to describe each LiveLesson used with it. We could reuse that file, but it's simpler in Cocoa to use a property list.
Controlling the Contents
The property list that describes the lesson is stored in the .app bundle's Resources directory. Cocoa provides some convenient methods for finding and loading the list:
NSBundle *mainBundle = [NSBundle mainBundle]; // Get the path of the project plist NSString *path = [mainBundle pathForResource: @"project" ofType: @"plist"]; // Load it as a dictionary lessonDescription = [[NSDictionary dictionaryWithContentsOfFile: path] retain];
First we get the object that encapsulates the application's main bundle. Next, we use that bundle to find the path of the project.plist file. This is stored as an XML property list (or you could use a binary or OpenStep-style plist), which we can edit in a text editor or in XCode's graphical property list editor.
Finally, we use a convenience method on NSDictionary that constructs a new dictionary instance from the contents of the property list file. Then we can use the dictionary just like any other dictionary. It's trivial to add new keys to the code and add the corresponding data to the property list; you don't need to write any code for parsing configuration files.
This is all of the code needed to load the list of lessons to populate the outline view:
// Set the lesson dictionary in a KVO-compatible way // so that the bindings notice the change [self willChangeValueForKey: @"children"]; children = [lessonDescription objectForKey: @"Lessons"]; [self didChangeValueForKey: @"children"];
The lessons are stored as a tree in the property list. The Lessons key refers to an array containing one item for each of the 12 lessons. Figure 3 shows the project.plist file in XCode's property list viewer. Notice that each lesson is a dictionary and contains an array for the children key. This array has a child for each video in the series, containing the video filename and some metadata.
Figure 3 The project property list in XCode.
If you look in the nib file, you can see how this design populates the outline view without requiring any code. The outline view column is connected with Cocoa bindings to an NSTreeController instance. The tree controller is bound to the children key in our object, so setting this key in a way that respects key-value observing means that it's automatically picked up by the controller.
This same pattern is used with the chapters dictionary from each lesson in the property list, populating the pop-up button, which allows the user to jump to different chapters in the file.