TweetFollow Us on Twitter

July 02 Mac OS X

Volume Number: 18 (2002)
Issue Number: 7
Column Tag: Mac OS X

Writing Plug-Ins for Cocoa: iPhoto to PhotoToWeb Exporter Plug-In

by Andrew Stone

After you've been working with Cocoa for a while, you'll come to appreciate the truth behind this statement: "If it's hard to do, then you're not doing it right”. By this I mean any coding solution that involves convoluted logic, going beneath the API or using undocumented methods, is probably not the right approach.

However, sometimes delving into secret API is the fastest way to get something done. Caveat Emptor: your program may break if the underlying methods change! That's why I usually admonish against using private, hidden methods.

This article will explain how to peek into the guts of an Objective C Cocoa application, iPhoto, and write a PlugIn that allows users to export their photos to the Stone Studio digital photography to web application PhotoToWeb®. iPhoto offers only limited web production facilities, whereas PhotoToWeb offers a complete suite of web design functionality, including the ability to create linked multi-level photo websites.

My design objectives were to keep the PlugIn small and very simple, including using the existing PhotoToWeb AppleScript API. This way, there are no code or file format dependencies in the PlugIn. Instead, we simply build a script that asks PhotoToWeb to build a new album with the exported photos. Also, instead of duplicating the user's photos, we just export the full path names since PhotoToWeb can build albums out of any images anywhere on the user's disk or network.


Figure 1: iPhoto loads the PhotoToWeb export PlugIn via Share - Export

Class-Dump Is Your Friend

If you want to see the inner architecture of an Objective C application, you'll need class-dump, which you can download from:

http://www.omnigroup.com/~nygard/Projects/index.html

class-dump is a utility for examining the Objective-C segment of Mach-O files. It generates the @interface and @protocol declarations for classes, categories and protocols.

Class-dump is open source, and has been an ongoing project since the early NeXT (the grandfather of OS X) days. It has had several contributors, including Eric P. Scott, Steve Nygard, James McIlree, Tom Hageman, and Carl Lindberg. It's a unix executable and has no GUI. Running it without arguments gives you the options:

class-dump 2.1.5

Usage: class-dump [-a] [-A] [-e] [-R] [-C regex] [-r] [-s] [-S] executable-file
        -a  show instance variable offsets
        -A  show implementation addresses
        -e  expand structure (and union) definition whenever possible
        -R  recursively expand @protocol <>
        -C  only display classes matching regular expression
        -r  recursively expand frameworks and fixed VM shared libraries
        -s  convert STR to char *
        -S  sort protocols, classes, and methods

You can dump all the classes of an application, or just those matching some pattern. Since we're interested in the Export methods, we'll run it like this:

class-dump -C Export /Applications/iPhoto.app/Contents/MacOSX/iPhoto
which outputs:
@protocol ExportImageProtocol
- directoryPath;
- (void)cancelExportBeforeBeginning;
- (void)cancelExport;
- (void)startExport;
- (void)clickExport;
- (void)disableControls;
- (void)enableControls;
- window;
- getImageDictionaryAtIndex:(unsigned int)fp16;
- getThumbPathAtIndex:(unsigned int)fp16;
- getImagePathAtIndex:(unsigned int)fp16;
- (char)imageIsPortraitAtIndex:(unsigned int)fp16;
- imageSelectionArray;
- (unsigned int)imageCount;
@end
@interface ExportMgr:NSObject <ExportImageProtocol>
{
    ArchiveDocument *mDocument;
    NSMutableArray *mExporters;
    KeyMgr *mSelection;
    ExportController *mExportController;
}
+ exportMgr;
- (void)releasePlugins;
- (void)setExportController:fp12;
- exportController;
- (void)setDocument:fp12;
- document;
- (void)updateDocumentSelection;
- (unsigned int)count;
- recAtIndex:(unsigned int)fp12;
- (void)scanForExporters;
- (unsigned int)imageCount;
- imageSelectionArray;
- (char)imageIsPortraitAtIndex:(unsigned int)fp12;
- getImagePathAtIndex:(unsigned int)fp12;
- getThumbPathAtIndex:(unsigned int)fp12;
- getImageDictionaryAtIndex:(unsigned int)fp12;
- getImageRecAtIndex:(unsigned int)fp12;
- (void)enableControls;
- (void)disableControls;
- window;
- (void)clickExport;
- (void)startExport;
- (void)cancelExport;
- (void)cancelExportBeforeBeginning;
- directoryPath;
- (void)_copySelection:fp12;
@end

So all we need to do to write a PlugIn is implement the methods defined in the ExportImageProtocol, add a few keys to the Info.plist file, and provide an interface file with any special controls. You can open some of the existing PlugIns inside iPhoto by control-clicking iPhoto in Finder, looking inside the Contents/PlugIns folder, and control-clicking one of the Exporter bundles.

Of course, without documentation or source, we'll have to guess at what certain methods do. ExportMgr has a class method "+exportMgr” which provides us with the ExportMgr instance. Reading over the ExportMgr class methods, we see methods for retrieving a set of photos one at a time, and getting information about that photo.

Making PlugIns

To make a loadable bundle (which is exactly what a PlugIn is), launch Project Builder and choose New Project..., select "Bundle”, provide a path, and save. Create a new empty nib in InterfaceBuilder, add a CustomView (we really don't need a window, just a view containing our interface elements), and save it into your bundle project.

We only need one class, "PhotoToWebExport”, but we also need to include the protocol as defined in ExportPluginProtocol, and declare that our object conforms to this protocol:

/* PhotoToWebExport.h */
#import <Cocoa/Cocoa.h>
@protocol ExportPluginProtocol
- (void)cancelExport;
- (void)unlockProgress;
- (void)lockProgress;
- (void *)progress;
- (void)performExport:fp16;
- (void)startExport:fp16;
- defaultDirectory;
- defaultFileName;
- requiredFileType;
- (void)viewWillBeDeactivated;
- (void)viewWillBeActivated;
- lastView;
- firstView;
- settingsView;
- initWithExportImageObj:fp16;
@end
#define IS_NULL(s) (!s || [s isEqualToString:@””])
#define NOT_NULL(s) (s && ![s isEqualToString:@””])
@interface PhotoToWebExport : NSObject <ExportPluginProtocol>
{
    IBOutlet id settingsView;
    IBOutlet id titleField;
    IBOutlet id imageView;
    IBOutlet id actionMatrix;
    IBOutlet id finishView;
    IBOutlet id firstView;
    id exportManager;
    ComponentInstance myComponent;
    NSString *_lastDirectoryPath;
}
- (void)downloadLatest:(id)sender;
@end
#import "PhotoToWebExport.h”
#import <Carbon/Carbon.h>
@implementation PhotoToWebExport
// Designated initializer 
// we'll grab the ExportMgr instance - although you could
// just use the class method + [ExportMgr exportMgr]
- initWithExportImageObj:fp16 {    
    self = [super init];
    exportManager = fp16;
   // we'll use AppleScript to talk to PhotoToWeb:
    myComponent = OpenDefaultComponent(kOSAComponentType, kOSAGenericScriptingComponentSubtype);
    return self;
}
// if there are photos to export, create and run an AppleScript
// to load the photos into a new PhotoToWeb album:
- (void)doExport:fp16 {
    BOOL good = YES;
    if ([exportManager imageCount] > 0) 
        [self runScript:[self scriptCommand]];
    else {
        NSRunAlertPanel(@”No images selected”,@”No images to export”,@”OK”, nil, nil);
        good = NO;
    }
    
    // make panel disappear - might be other way:
    [[exportManager exportController] cancel:self];
   
   // Bring the album to the front so user can make web site or thumbnails etc...
    if (good) [[NSWorkspace sharedWorkspace] openFile:[exportManager directoryPath] 
    withApplication:@”PhotoToWeb”];
}
// this is the guts of the plugin 
// We'll create the AppleScript script necessary to export
// the selected photos, and attempt to grab as much information
// as the users have provided in iPhoto - such as comments, rotation
// crop information, title, etc.
- (NSString *)scriptCommand {
    NSMutableString *s = [NSMutableString stringWithCapacity:2000];
    unsigned i, c = [exportManager imageCount];
    
    // for next time the user runs it, we'll have their preferred directory:
    [_lastDirectoryPath release];
    _lastDirectoryPath = [[[exportManager directoryPath] stringByDeletingLastPathComponent] 
    copyWithZone:[self zone]];
    [s appendString:@”tell application \”PhotoToWeb\”\r”];
    [s appendString:@”\tmake new album at before front album\r”];
    [s appendString:@”\ttell the front album\r”];
// this will prevent the AppleScript from timing out if there is a huge number of photos:
    [s appendString:@”\t\tignoring application responses\r”];
    
    for (i = 0; i < c; i++) {
        NSString *path = [exportManager getImagePathAtIndex:i];
        NSDictionary *dict = [exportManager getImageDictionaryAtIndex:i];
        int rotation = [[dict objectForKey:@”Rotation”] intValue];
        NSString *title = [dict objectForKey:@”Caption”];
        NSString *story = [dict objectForKey:@”Annotation”]; // could be nil
        NSDate *archiveDate = [dict objectForKey:@”ArchiveDate”];
        NSString *date = [[NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
        [archiveDate timeIntervalSinceReferenceDate]] descriptionWithCalendarFormat:@”Archived: 
        %e %B %Y”];
        [s appendString:[NSString stringWithFormat:@”\t\t\tadd photos in \”%@\”\r”, path]];
        if (rotation != 0) {
                [s appendString:[NSString stringWithFormat:@”\t\t\tset the photorotation of last photo 
                to %d\r”, rotation]];
        }
        if (NOT_NULL(title)) 
            [s appendString:[NSString stringWithFormat:@”\t\t\tset the title of last photo to 
            \”%@\”\r”, title]];
        if (NOT_NULL(story)) {
            [s appendString:[NSString stringWithFormat:@”\t\t\tset the text contents of last photo to 
            \”%@\”\r”, NOT_NULL(date)? [NSString stringWithFormat: @”%@ %@”,story,date]:story]];
        } else if (NOT_NULL(date))
            [s appendString:[NSString stringWithFormat:@”\t\t\tset the text contents of last photo to 
            \”%@\”\r”, date]];
    
    }
    [s appendString:@”\t\tend ignoring\r”];
    [s appendString:[NSString stringWithFormat:@”\t\tsave in \”%@\”\r”,[exportManager directoryPath]]];
    [s appendString:@”\tend tell\r”];
    [s appendString:@”end tell\r”];
    return s;
}
- (void)performExport:fp16  {
    // not sure how this differs from startExport but doesn't seem to be needed
}
- (void)startExport:fp16 {
    [self doExport:fp16];
}
// if the plugin has been run before, use the last saved path
// Otherwise choose a standard location:
- (id) defaultDirectory {
    if (NOT_NULL(_lastDirectoryPath)) return _lastDirectoryPath;
   return [NSHomeDirectory() stringByAppendingPathComponent:@”Documents”];
}
// here's a nice way to guarantee uniqueness of the album name
// concatenate the year-month-day-seconds:
- defaultFileName {
    return [NSString stringWithFormat:@”%@-%d”,[[NSCalendarDate calendarDate] 
    descriptionWithCalendarFormat:@”%Y-%m-%d”], [NSDate timeIntervalSinceReferenceDate]];
}
// this is PhotoToWeb's single Album file type:
- requiredFileType {
    return @”album”;
}
- lastView {
   return finishView;
}
- firstView {
   return firstView;
}
- settingsView {
   return settingsView;
}
// An action method to provide user with latest version of PhotoToWeb:
- (void)downloadLatest:(id)sender {
            [[NSWorkspace sharedWorkspace] openURL:[NSURL
            URLWithString:@”http://www.stone.com/NewDownload.html#PHOTO”]];
}
// I can't think of anything to do in these two methods
// In a more complex plugin, you might set default values:
- (void)viewWillBeDeactivated {
}
- (void)viewWillBeActivated {
}
// these next 4 methods are not yet flushed out because I haven't figured
// out exactly what to do with them - if you do, email me!
- (void)cancelExport {
}
- (void)unlockProgress {
}
- (void)lockProgress {
}
- (void *)progress {
    return NULL; //??
}
// The AppleScript compilation methods:
#define CHECK  
//fprintf(stderr,”result code = %d”, ok);
// This converts an AEDesc into a corresponding NSValue.
static id aedesc_to_id(AEDesc *desc)
{
    OSErr ok;
    
    if (desc->descriptorType == typeChar)
    {
        NSMutableData *outBytes;
        NSString *txt;
        
        outBytes = [[NSMutableData alloc] initWithLength:AEGetDescDataSize(desc)];
        ok = AEGetDescData(desc, [outBytes mutableBytes], [outBytes length]);
        CHECK;
    
        txt = [[NSString alloc] initWithData:outBytes encoding:[NSString defaultCStringEncoding]];
        [outBytes release];
        [txt autorelease];
        
        return txt;
    }
    
    if (desc->descriptorType == typeSInt16)
    {
        SInt16 buf;
        
        AEGetDescData(desc, &buf, sizeof(buf));
        
        return [NSNumber numberWithShort:buf];
    }
    
    return [NSString stringWithFormat:@”[unconverted AEDesc, type=\”%c%c%c%c\”]”, 
    ((char *)&(desc->descriptorType))[0], ((char *)&(desc->descriptorType))[1], 
    ((char *)&(desc->descriptorType))[2], ((char *)&(desc->descriptorType))[3]];
}
- (void)runScript:(NSString *)txt
{
    NSData *scriptChars = [txt dataUsingEncoding:[NSString defaultCStringEncoding]];
    AEDesc source, resultText;
    OSAID scriptId, resultId;
    OSErr ok;
    // Convert the source string into an AEDesc of string type.
    ok = AECreateDesc(typeChar, [scriptChars bytes], [scriptChars length], &source);
    CHECK;
    
    // Compile the source into a script.
    scriptId = kOSANullScript;
    ok = OSACompile(myComponent, &source, kOSAModeNull, &scriptId);
    AEDisposeDesc(&source);
    CHECK;
            
    // Execute the script, using defaults for everything.
    resultId = 0;
    ok = OSAExecute(myComponent, scriptId, kOSANullScript, kOSAModeNull, &resultId);
    CHECK;
    
    if (ok == errOSAScriptError) {
        AEDesc ernum, erstr;
        id ernumobj, erstrobj;
        
        // Extract the error number and error message from our scripting component.
        ok = OSAScriptError(myComponent, kOSAErrorNumber, typeShortInteger, &ernum);
        CHECK;
        ok = OSAScriptError(myComponent, kOSAErrorMessage, typeChar, &erstr);
        CHECK;
        
        // Convert them to ObjC types.
        ernumobj = aedesc_to_id(&ernum);
        AEDisposeDesc(&ernum);
        erstrobj = aedesc_to_id(&erstr);
        AEDisposeDesc(&erstr);
        
        txt = [NSString stringWithFormat:@”Error, number=%@, message=%@”, ernumobj, erstrobj];
    } else {
        // If no error, extract the result, and convert it to a string for display
        
        if (resultId != 0) { // apple doesn't mention that this can be 0?
            ok = OSADisplay(myComponent, resultId, typeChar, kOSAModeNull, &resultText);
            CHECK;
    
            txt = aedesc_to_id(&resultText);
            AEDisposeDesc(&resultText);
        } else {
            txt = @”[no value returned]”;
        }
        OSADispose(myComponent, resultId);
    }
    
    ok = OSADispose(myComponent, scriptId);
    CHECK;
}
@end

After you compile your PlugIn, drop it into the PlugIns folder in iPhoto, relaunch iPhoto, choose "Share” and "Export”, and if all is well, you'll see your PlugIn as a tab in the Export TabView window.

Then Apple Released iPhoto 1.1

Before this article went to print, Apple released an update to iPhoto—and indeed, my Exporter bundle broke! First, I looked through the console error messages and saw that my PlugIn wasn't responding to certain messages. Then, I used class-dump to confirm that some protocols had changed.

The first problem was that my PlugIn's main settingsView didn't respond to the ExportPluginBoxProtocol protocol:

@protocol ExportPluginBoxProtocol
- (char)performKeyEquivalent:fp16;
@end

So, by looking at the nib files in the Apple-provided PlugIns, I deduced this simple NSBox subclass:

@interface P2WExportPluginBox:NSBox <ExportPluginBoxProtocol>
{
    id mPlugin;
}
@end
@implementation P2WExportPluginBox
- (char)performKeyEquivalent:fp12 {
}
@end

Drop this source file into the Interface Builder class window to have it read the file. Assign the class "P2WExportPluginBox” to the NSBox containing your PlugIn controls in Interface Builder. Connect the "mPlugin” instance variable to the file's owner. Save, build, and drop into the PlugIns folder. (An interesting sidenote: variable names can reveal a lot. The coder here was probably an old Mac toolbox programmer, as indicated by the lower-case "m”, as in "my”, for the ivar. Objective-C coders conventionally use an underscore to prefix ivars, so the ivar name would have been "_plugin”.)

Second, a few more methods were needed in the main PlugIn class to get it to work again:

@implementation PhotoToWebExport
- (NSString *)name {
    return @”Stone Design's iPhoto to PhotoToWeb Exporter”;
}
- (BOOL)treatSingleSelectionDifferently { return NO; }
...
 

Conclusion

Cocoa, especially the Objective C flavor, is the future of Mac OS X, as evidenced by the new apps coming out of Apple written almost entirely in Cocoa. Dynamic runtime loading comes free, and you can examine the classes and methods in an application using class-dump. Knowledge of Carbon is still very useful. For example, in this PlugIn we use it to compile and execute our AppleScript. It's a good idea to keep an agnostic attitude towards the various Apple API's.

Using the techniques outlined in this article, you should be able to add all sorts of functionality to the ever-increasing number of applications available for Mac OS X.


Andrew Stone

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Duplicate Annihilator 5.7.5 - Find and d...
Duplicate Annihilator takes on the time-consuming task of comparing the images in your iPhoto library using effective algorithms to make sure that no duplicate escapes. Duplicate Annihilator... Read more
BusyContacts 1.0.2 - Fast, efficient con...
BusyContacts is a contact manager for OS X that makes creating, finding, and managing contacts faster and more efficient. It brings to contact management the same power, flexibility, and sharing... Read more
Capture One Pro 8.2.0.82 - RAW workflow...
Capture One Pro 8 is a professional RAW converter offering you ultimate image quality with accurate colors and incredible detail from more than 300 high-end cameras -- straight out of the box. It... Read more
Backblaze 4.0.0.872 - Online backup serv...
Backblaze is an online backup service designed from the ground-up for the Mac.With unlimited storage available for $5 per month, as well as a free 15-day trial, peace of mind is within reach with... Read more
Little Snitch 3.5.2 - Alerts you about o...
Little Snitch gives you control over your private outgoing data. Track background activity As soon as your computer connects to the Internet, applications often have permission to send any... Read more
Monolingual 1.6.4 - Remove unwanted OS X...
Monolingual is a program for removing unnecesary language resources from OS X, in order to reclaim several hundred megabytes of disk space. If you use your computer in only one (human) language, you... Read more
CleanApp 5.0 - Application deinstaller a...
CleanApp is an application deinstaller and archiver.... Your hard drive gets fuller day by day, but do you know why? CleanApp 5 provides you with insights how to reclaim disk space. There are... Read more
Fantastical 2.0 - Create calendar events...
Fantastical is the Mac calendar you'll actually enjoy using. Creating an event with Fantastical is quick, easy, and fun: Open Fantastical with a single click or keystroke Type in your event details... Read more
Cocktail 8.2 - General maintenance and o...
Cocktail is a general purpose utility for OS X that lets you clean, repair and optimize your Mac. It is a powerful digital toolset that helps hundreds of thousands of Mac users around the world get... Read more
Direct Mail 4.0.4 - Create and send grea...
Direct Mail is an easy-to-use, fully-featured email marketing app purpose-built for OS X. It lets you create and send great looking email campaigns. Start your newsletter by selecting from a gallery... Read more

Fast & Furious: Legacy's Creati...
| Read more »
N-Fusion and 505's Ember is Totally...
| Read more »
These are All the Apple Watch Apps and G...
The Apple Watch is less than a month from hitting store shelves, and once you get your hands on it you're probably going to want some apps and games to install. Fear not! We've compiled a list of all the Apple Watch apps and games we've been able to... | Read more »
Appy to Have Known You - Lee Hamlet Look...
Being at 148Apps these past 2 years has been an awesome experience that has taught me a great deal, and working with such a great team has been a privilege. Thank you to Rob Rich, and to both Rob LeFebvre and Jeff Scott before him, for helping me... | Read more »
Hands-On With Allstar Heroes - A Promisi...
Let’s get this out of the way quickly. Allstar Heroes looks a lot like a certain other recent action RPG release, but it turns out that while it’s not yet available here, Allstar Heroes has been around for much longer than that other title. Now that... | Read more »
Macho Man and Steve Austin Join the Rank...
WWE Immortals, by Warner Bros. Interactive Entertainment and WWE, has gotten a superstar update. You'll now have access to Macho Man Randy Savage and Steve Austin. Both characters have two different versions: Macho Man Randy Savage Renegade or Macho... | Read more »
Fearless Fantasy is Fantastic for the iF...
I actually had my first look at Fearless Fantasy last year at E3, but it was on a PC so there wasn't much for me to talk about. But now that I've been able to play with a pre-release version of the iOS build, there's quite a bit for me to talk... | Read more »
MLB Manager 2015 (Games)
MLB Manager 2015 5.0.14 Device: iOS Universal Category: Games Price: $4.99, Version: 5.0.14 (iTunes) Description: Guide your favorite MLB franchise to glory! MLB Manager 2015, officially licensed by MLB.com and based on the award-... | Read more »
Breath of Light (Games)
Breath of Light 1.0.1421 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0.1421 (iTunes) Description: Hold a quiet moment. Breath of Light is a meditative and beautiful puzzle game with a hypnotic soundtrack by... | Read more »
WWE WrestleMania Tags into the App Store
Are You ready to rumble? The official WWE WrestleMania app, by World Wrestling Entertainment, is now available. Now you can get all your WrestleMania info in one place before anyone else. The app offers details on superstar signings, interactive... | Read more »

Price Scanner via MacPrices.net

13-inch 2.5GHz MacBook Pro (refurbished) avai...
The Apple Store has Apple Certified Refurbished 13″ 2.5GHz MacBook Pros available for $829, or $270 off the cost of new models. Apple’s one-year warranty is standard, and shipping is free: - 13″ 2.... Read more
Save up to $80 on iPad Air 2s, NY tax only, f...
 B&H Photo has iPad Air 2s on sale for $80 off MSRP including free shipping plus NY sales tax only: - 16GB iPad Air 2 WiFi: $469.99 $30 off - 64GB iPad Air 2 WiFi: $549.99 $50 off - 128GB iPad... Read more
iMacs on sale for up to $205 off MSRP
B&H Photo has 21″ and 27″ iMacs on sale for up to $205 off MSRP including free shipping plus NY sales tax only: - 21″ 1.4GHz iMac: $1019 $80 off - 21″ 2.7GHz iMac: $1189 $110 off - 21″ 2.9GHz... Read more
Färbe Technik Offers iPhone Battery Charge LI...
Färbe Technik, which manufactures and markets of mobile accessories for Apple, Blackberry and Samsung mobile devices, is offering tips on how to keep your iPhone charged while in the field: •... Read more
Electronic Recyclers International CEO Urges...
Citing a recent story on CNBC about concerns some security professionals have about the forthcoming Apple Watch, John Shegerian, Chairman and CEO of Electronic Recyclers International (ERI), the... Read more
Save up to $380 with Apple refurbished iMacs
The Apple Store has Apple Certified Refurbished iMacs available for up to $380 off the cost of new models. Apple’s one-year warranty is standard, and shipping is free: - 27″ 3.5GHz 5K iMac – $2119 $... Read more
Mac minis on sale for up to $75 off, starting...
MacMall has Mac minis on sale for up to $75 off MSRP including free shipping. Their prices are the lowest available for these models from any reseller: - 1.4GHz Mac mini: $459.99 $40 off - 2.6GHz Mac... Read more
College Student Deals: Additional $50 off Mac...
Take an additional $50 off all MacBooks and iMacs at Best Buy Online with their College Students Deals Savings, valid through April 11, 2015. Anyone with a valid .EDU email address can take advantage... Read more
Mac Pros on sale for up to $260 off MSRP
B&H Photo has Mac Pros on sale for up to $260 off MSRP. Shipping is free, and B&H charges sales tax in NY only: - 3.7GHz 4-core Mac Pro: $2799, $200 off MSRP - 3.5GHz 6-core Mac Pro: $3719.99... Read more
13-inch 2.5GHz MacBook Pro on sale for $100 o...
B&H Photo has the 13″ 2.5GHz MacBook Pro on sale for $999 including free shipping plus NY sales tax only. Their price is $100 off MSRP. Read more

Jobs Board

DevOps Software Engineer - *Apple* Pay, iOS...
**Job Summary** Imagine what you could do here. At Apple , great ideas have a way of becoming great products, services, and customer experiences very quickly. Bring Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
Sr. Technical Services Consultant, *Apple*...
**Job Summary** Apple Professional Services (APS) has an opening for a senior technical position that contributes to Apple 's efforts for strategic and transactional Read more
Lead *Apple* Solutions Consultant - Retail...
**Job Summary** Job Summary The Lead ASC is an Apple employee who serves as the Apple business manager and influencer in a hyper-business critical Reseller's store Read more
*Apple* Pay - Site Reliability Engineer - Ap...
**Job Summary** Imagine what you could do here. At Apple , great ideas have a way of becoming great products, services, and customer experiences very quickly. Bring Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.