TweetFollow Us on Twitter

The Road to Code: One for the Archives

Volume Number: 24 (2008)
Issue Number: 07
Column Tag: The Road to Code

The Road to Code: One for the Archives

Archiving objects and document—based applications

by Dave Dribin

One Year Later

This is the thirteenth article in the Road to Code column: happy one—year anniversary! We've covered a lot of ground in the last year, but we've still got the pedal to the metal. There's plenty of pavement left to cover!

In last month's article, I said that we'd talk more about NSTableView in this article, but I've decided to put that topic off for a bit. This month we're going to cover document—based applications, which includes how to save and open – or load – custom file types.

Document—based applications are a class of Mac OS X applications that allow the user to create, save, and open documents. Since this is a common type of application, Cocoa provides a lot of the common architecture, or plumbing, to minimize the amount of code the developer has to write.

Archiving

Before we talk about the user interface help that Cocoa provides for document—based applications, we're going to start from the back—end and talk about saving and loading objects. The Foundation framework provides a mechanism for converting a collection of objects to a sequence of bytes called archiving. Once we have the objects as a sequence of bytes, these bytes can be stored on disk or even sent across the network. To recreate the collection of objects from the sequence of bytes, you can use a mechanism called unarchiving. In Java, this conversion of objects to and from bytes is called serialization.

Before we get into the nitty—gritty, we need to cover some more terminology. A collection of objects has a fancy name called object graph. An object graph is a collection of objects, along with the relationship between the objects. An object graph can be represented as a diagram showing each object, with arrows indicating relationships. For example, if we have an NSMutableArray object that contains three Rectangle objects, the object graph would look like Figure 1. The arrows point from the array to the rectangles because the array maintains a reference to each of its members. Often, an object graph contains one object that is at the top of the graph called the root object. In this case, the array is the root object.


Figure 1: Simple object graph

Recall from the article about inheritance, You Have Your Mother's Eyes, that Objective—C has a concept called a protocol. As a quick refresher, protocols are just like class interface declarations, except that there are no instance variables. Protocols also have no implementation. Other classes may implement protocols, and these classes must provide implementations to all required methods. Protocols are used to ensure a class implements a set of methods.

Encoding

Foundation's archiving and unarchiving works on object graphs. Not all objects may be archived, however. Only objects that implement the NSCoding protocol may be archived and unarchived. The NSCoding protocol is shown in Listing 1.

Listing 1: NSCoding protocol

@protocol NSCoding
— (void)encodeWithCoder:(NSCoder *)coder;
— (id)initWithCoder:(NSCoder *)decoder;
@end

The encodeWithCoder: method is used for archiving while initWithCoder: is used for unarchiving. You'll notice that both of these methods use a class called NSCoder. NSCoder and its subclasses are responsible for the actual transformation to and from bytes. An example will help clear this up. Let's modify our Rectangle class to work with archiving. The most recent interface for Rectangle from the last article is shown in Listing 2.

Listing 2: Rectangle interface, without NSCoding support

#import <Foundation/Foundation.h>
@interface Rectangle : NSObject
{
    float _leftX;
    float _bottomY;
    float _width;
    float _height;
}
@property float leftX;
@property float bottomY;
@property float width;
@property float height;
@property (readonly) float area;
@property (readonly) float perimeter;
— (id)    initWithLeftX: (float) leftX
                bottomY: (float) bottomY
                 rightX: (float) rightX
                   topY: (float) topY;
@end

Since Rectangle's superclass, NSObject, does not implement the NSCoding protocol, the first step is to tell the compiler that we want to implement the NSCoding by changing the @interface declaration:

@interface Rectangle : NSObject <NSCoding>

The rest of the header file stays the same. We must now implement encodeWithCoder: and initWithCoder: or else the compiler will warn us about an incomplete implementation. This warning is very helpful and one of the benefits of statically typed languages. Okay, so how do we implement these methods? Let's start with archiving and encodeWithCoder:.

When encodeWithCoder: is called on your object, you need to save all your vital instance variables by encoding their values in the coder. Vital instance variables are all instance variables that cannot be calculated in any way. For our Rectangle class, all four instance variables are vital, and we must encode them all. Our implementation would be:

— (void) encodeWithCoder: (NSCoder *) coder
{
    [coder encodeFloat: _leftX forKey: @"leftX"];
    [coder encodeFloat: _bottomY forKey: @"bottomY"];
    [coder encodeFloat: _width forKey: @"width"];
    [coder encodeFloat: _height forKey: @"height"];
}

We encode each of our instance variables, one by one, using the encodeFloat:forKey: method of NSCoder. We are using a variant of encoding called keyed encoding. Keyed encoding associates each value with a string key, or name, similar to how NSDictionary maps keys and values. All keys in an inheritance chain must be unique. This means your key must not only be unique to all your encoded values, but also to any of your superclasses' encoded values. The simplest way to ensure this is to use the name of the instance variable. I tend to leave off the underscore prefix, but that's just old habit. Separate inheritance chains can reuse keys. For example a Bitmap class that inherits from NSObject could also encode values using the width and height keys without conflict from Rectangle.

There are encoding methods for all primitive types and for other objects that implement the NSCoding protocol. Many of the classes in the Foundation framework such as strings, arrays, and dictionaries implement NSCoding so you should be able to easily encode all of your instance variables. Don't forget that if your superclass implements NSCoding, you must call encodeWithCoder: on the superclass before you encode your instance variables to make sure that all of its instance variables are encoded, as well:

— (void) encodeWithCoder: (NSCoder *) coder
{
    [super encodeWithCoder: coder];
    //...
}

As a side note, NSCoder does allow encoding values without keys, called non—keyed encoding, however this method is no longer recommended. Keyed encoding was introduced in Mac OS X 10.2 and non—keyed encoding should only be used for legacy applications that either need to run on or interoperate with old versions of software. Since we are writing an application for Mac OS X 10.5, we only use keyed encoding.

To use this method, we need an NSCoder instance. NSCoder is an abstract class, meaning that you cannot create instances of it directly; rather, you should use one of its concrete subclasses. The main subclass for encoding is NSKeyedArchiver, and typically, you use one of its class methods that do all the hard work for you:

    Rectangle * rectangle = ...;
    NSData * data =
        [NSKeyedArchiver archivedDataWithRootObject: rectangle];

The NSData class is part of the Foundation framework and it holds a sequence of bytes. Once you have the rectangle represented as an NSData instance, you can write it to disk or send it over a network.

Decoding

Now that we've covered archiving objects by encoding objects with an NSCoder instance, we need to go the other way around. Unarchiving objects takes a sequence of bytes and creates new instances of the encoded objects. To support unarchiving, our class must implement the initWithCoder: method. For our Rectangle class, there are no real surprises. We must decode each instance variable using the same key we used for encoding:

— (id) initWithCoder: (NSCoder *) decoder
{ 
    self = [super init];
    if (self == nil)
        return nil;
    
    _leftX = [decoder decodeFloatForKey: @"leftX"];
    _bottomY = [decoder decodeFloatForKey: @"bottomY"];
    _width = [decoder decodeFloatForKey: @"width"];
    _height = [decoder decodeFloatForKey: @"height"];
    
    return self;
}

Note that this is a constructor method and thus creates a new instance of our object. Since our superclass is NSObject and it does not implement NSCoding, we just call init. However, if your superclass also supports NSCoding, be sure to call initWithCoder: instead:

— (id) initWithCoder: (NSCoder *) decoder
{ 
    self = [super initWithCoder: decoder];
    //...
}

While you can decode the keys in any order, it's imperative that the keys match up between your encoding and decoding. Also, if you decode any objects, you must remember to retain them if you keep strong references to them as the decodeObjectForKey: returns autoreleased objects. Of course, if you are using garbage collection, you don't have to worry about the retains.

To unarchive objects from a sequence bytes created with NSKeyedArchiver, you should use the NSKeyedUnarchiver, which is also a concrete subclass of NSCoder. It has a convenient class method to unarchive an object directly from an NSData instance:

    NSData * data = ...;
    Rectangle * rectangle =
        [NSKeyedUnarchiver unarchiveObjectWithData: data];

Now that our Rectangle class fully implements the NSCoding protocol, we can convert instances of our class to a sequence of bytes and back again.

Archiving objects has many uses. For example, Interface Builder nib files are really just archived objects. The object graph for your entire GUI is created by Interface Builder and saved as a nib file. When your application runs, the objects are unarchived and ready to use. We will also be using this new ability to create custom rectangle documents using a document—based application.

You may be wondering what happens if we add or remove instance variables to our rectangle class? And what if the newer rectangle class needs to read archives saved by the older rectangle class? The way to handle this is with a technique called versioning. Versioning is a bit advanced to cover in full here, but I just wanted to let you know that it is possible to handle this situation. Read Apple's developer documentation for full details.

Document—Based Applications

As I mentioned above, many applications allow the user to create and edit documents, and then save them to and open them from disk. Since this is such a common type of application, Cocoa provides an architecture to help with the common tasks of managing documents. Applications that utilize this architecture are called document—based applications. The three classes that make up this architecture are NSDocument, NSWindowController, and NSDocumentController and are found in the Application Kit framework, along with all the GUI classes such as NSView and NSControl.

For simple cases, you generally don't have to interact with NSWindowController or NSDocumentController. However, you must always subclass NSDocument to implement saving and loading. We're going to walk through a simple document—based application that allows a user to save and load rectangle data.

Xcode has a project template for document—based applications, so we're going create a fresh project from this template. Select File > New ProjectÉ from Xcode and choose a Cocoa Document—based Application from the New Project assistant, as shown in Figure 2. Click Next, and create a project named Rectangles in the directory of your choice. Finally, enable garbage collection by modifying the project's build settings.


Figure 2: Creating a document—based application

If you take a look at the Groups & Files list for this project, you'll notice that it is slightly different than the non—document—based applications we've created so far, as shown in Figure 3. First, it creates a subclass of NSDocument called MyDocument for us in the Classes group. Second, it creates two nib files in the Resources group: MainMenu.nib and MyDocument.nib.


Figure 3: Generated files

We've previously only used a single nib file that contained both the main menu and the single application window. Because we can have multiple document windows open, we use a separate nib file for document windows. Remember that nib files are archived windows, views, and controls, so every time a new document window is created, Cocoa's document architecture unarchives MyDocument.nib to create a new window. The MainMenu.nib file only gets loaded once at application startup and contains only the menu bar items.

You can already run the application as—is, but it's quite limited. First, the window contains a nice message that you are supposed to add your own controls to it. Also, saving and loading are not implemented, and you get an error if you try and save. You can, however, create new documents using File > New or Command—N.

Creating the User Interface

We're going to start by making the document window look just like the window of our Hello World application. Figure 4 shows the final result. Open MyDocument.nib with Interface Builder and layout the controls and formatters like we did before. Also setup the proper resizing springs and struts. (See last month's article if you need a reminder of how to do this.)


Figure 4: MyDocument.nib window

The MyDocument class takes the place of HelloWorldController from the previous articles. It contains all the outlets and actions for the window. Add outlets for the four text fields, a calculate action, and an instance variable for a single rectangle, as shown in Listing 3:

Listing 3: MyDocument.h

#import <Cocoa/Cocoa.h>
@class Rectangle;
@interface MyDocument : NSDocument
{
    IBOutlet NSTextField * _widthField;
    IBOutlet NSTextField * _heightField;
    IBOutlet NSTextField * _areaLabel;
    IBOutlet NSTextField * _perimiterLabel;
    
    Rectangle * _rectangle;
}
— (IBAction)calculate:(id)sender;
@end

Now save MyDocument.h, and go back to Interface Builder. Connect up the outlets and actions to the appropriate controls. The File's Owner is setup to be our document class, MyDocument, so use this when connecting the outlets and actions.

Now, add the Rectangle class that we modified to support NSCoding protocol above to this project. In MyDocument, change the init method, and add the calculate: and updateAreaAndPerimeter methods to match this:

— (id)init
{
    self = [super init];
    if (self == nil)
        return nil;
    
    _rectangle = [[Rectangle alloc]    initWithLeftX: 0
                                             bottomY: 0
                                              rightX: 5
                                                topY: 10];
    
    return self;
}
— (void)updateAreaAndPerimeter
{
    [_areaLabel setFloatValue: _rectangle.area];
    [_perimiterLabel setFloatValue: _rectangle.perimeter];
}
— (IBAction)calculate:(id)sender
{
    _rectangle.width = [_widthField floatValue];
    _rectangle.height = [_heightField floatValue];
    [self updateAreaAndPerimeter];
}

This creates a new rectangle instance in the constructor and also implements the calculate: action. Remember, though, that we needed to implement the awakeFromNib to ensure the text fields and labels were correct on application launch. For NSDocument subclasses, you override the windowControllerDidLoadNib: instead of awakeFromNib. Their purpose is very similar, though, and allows you to execute code after the outlets and actions from the nib have been connected. Modify the supplied stub implementation to match this:

— (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
    [_widthField setFloatValue: _rectangle.width];
    [_heightField setFloatValue: _rectangle.height];
    [self updateAreaAndPerimeter];
}

Now build and run the application. You should see a window similar to Figure 5. The initial width should be five and height should be ten. If you change the width or height and press the Calculate button, the area and perimeter should update accordingly.


Figure 5: Running document window

This works like our first Hello World application. However, you can now create new windows with the File > New menu. We now want to implement saving and loading of documents.

Registering File Extensions

In order to identify our new file types, we need to come up with a new file extension. Since our document contains a single rectangle, let's use ".rectangle" as our file extension. We need to tell the operating system that our application can load and save files with this extension. This is done by editing the properties of our application. Open up the Targets group in the Groups & Files section of the project. Double click on the Rectangles target to bring up the Info panel for our application. Change to the Properties tab of the Info panel.

There are a couple of important properties we need to change. First, we need to change the identifier. Identifiers must be unique for every application released on Mac OS X. The recommended way to do this uses so—called reverse DNS notation. DNS is how websites are named. For example, my personal website is at www.dribin.org. The technical name for this website address is the DNS name. DNS names are unique, i.e. there is one and only one dribin.org out there. Reverse DNS takes advantage of the uniqueness of website names. It's called reverse DNS because you list the components in the opposite order that you use them for websites. The default value is com.yourcompany.Rectangles, which may be fine for testing, but you should really change it to a properly unique name.

Let's change it to org.dribin.dave.mactech.jul08.rectangles.

Next, we need to change the document types for our application. The Document Types list should already have one document type in it. Edit it so that the name is Rectangle File and that the extension is rectangle (do not include the period). The final result should look similar to Figure 6.


Figure 6: Application properties

These application properties are stored in a file called Info.plist. You can see this file in the Resources group. This file gets included along with your built application. How this works is beyond the scope of this article, but suffice it to say that applications on Mac OS X are really directories with a special structure called bundles. We will talk more about bundles in a later article.

One word of warning: I find that whenever I change the application's properties or Info.plist file, I need to force Xcode to rebuild the whole application. You do this by using the Build > Clean All Targets menu item. Cleaning removes all files generated during the build process. By cleaning the target, you force the next build to rebuild everything. Be sure to clean your project before proceeding.

Saving and Loading

With our file extension in place, we can now proceed to implement saving and loading of rectangle files. The MyDocument class already contains stub methods for saving and loading that we need to fill in. Saving is handled by the dataOfType:error: method. You are supposed to return an NSData representation of your document, and the document architecture will take care of writing it to a file. The default implementation returns nil and sets an error.

The only data we need to save is our _rectangle instance variable. Since the Rectangle class now implements the NSCoding protocol, we can use an NSKeyedArchiver to convert our instance variable into NSData:

— (NSData *)dataOfType:(NSString *)typeName
                 error:(NSError **)outError
{
    NSData * rectangleData =
        [NSKeyedArchiver archivedDataWithRootObject:_rectangle];
    return rectangleData;
}

In order to load rectangle files, we need to implement the readFromData:ofType:error: method. We can use NSKeyedArchiver to convert the supplied NSData to an NSRectangle and store it in our instance variable:

— (BOOL)readFromData:(NSData *)data
              ofType:(NSString *)typeName
               error:(NSError **)outError
{
    _rectangle = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    return YES;
}

If you are not using garbage collection, be sure to release the old rectangle and retain the new one.

With these two methods implemented, you should be able to save and open rectangle files. Give it a shot. The files should have the ".rectangle" extension we setup, too. You can even double click on saved rectangle files in the Finder, and it should automatically launch our application. As always, the completed project may be downloaded from the MacTech website.

Well, that's pretty much all there is to a document—based application. Due to the document architecture, there's really not that much extra code to write compared to non—document—based applications. Our MyDocument class is very similar to the HelloWorldController we wrote earlier. We just needed to add methods to save and load files, and that's really easy if the classes to be saved can be archived.

Given that we only had to implement two extra methods, we get a lot of functionality "for free" from the Cocoa document architecture:

A file open sheet is created when the user chooses File > OpenÉ

A file save sheet is created when the user choose File > Save or Save AsÉ

Saving and loading to and from disk is handled automatically

The window title changes to the name of the document

The document—based architecture really shows how Cocoa helps the developer write applications faster with less code. You can also get undo support easily, but that will have to wait for a future article.


Dave Dribin has been writing professional software for over eleven years. After five years programming embedded C in the telecom industry and a brief stint riding the Internet bubble, he decided to venture out on his own. Since 2001, he has been providing independent consulting services, and in 2006, he founded Bit Maki, Inc. Find out more at http://www.bitmaki.com/ and http://www.dribin.org/dave/.

 
AAPL
$112.65
Apple Inc.
+3.24
MSFT
$47.52
Microsoft Corpora
+1.78
GOOG
$511.10
Google Inc.
+6.21

MacTech Search:
Community Search:

Software Updates via MacUpdate

BBEdit 11.0.2 - Powerful text and HTML e...
BBEdit is the leading professional HTML and text editor for the Mac. Specifically crafted in response to the needs of Web authors and software developers, this award-winning product provides a... Read more
ExpanDrive 4.2.1 - Access cloud storage...
ExpanDrive builds cloud storage in every application, acts just like a USB drive plugged into your Mac. With ExpanDrive, you can securely access any remote file server directly from the Finder or... Read more
Adobe After Effects CC 2014 13.2 - Creat...
After Effects CC 2014 is available as part of Adobe Creative Cloud for as little as $19.99/month (or $9.99/month if you're a previous After Effects customer). After Effects CS6 is still available... Read more
Command-C 1.1.7 - Clipboard sharing tool...
Command-C is a revolutionary app which makes easy to share your clipboard between iOS and OS X using your local WiFi network, even if the app is not currently opened. Copy anything (text, pictures,... Read more
Tidy Up 4.0.2 - Find duplicate files and...
Tidy Up is a complete duplicate finder and disk-tidiness utility. With Tidy Up you can search for duplicate files and packages by the owner application, content, type, creator, extension, time... Read more
Typinator 6.3 - Speedy and reliable text...
Typinator turbo-charges your typing productivity. Type a little. Typinator does the rest. We've all faced projects that require repetitive typing tasks. With Typinator, you can store commonly used... Read more
GraphicConverter 9.5 - Graphics editor w...
GraphicConverter is an all-purpose image-editing program that can import 200 different graphic-based formats, edit the image, and export it to any of 80 available file formats. The high-end editing... Read more
Toast Titanium 12.0.1 - The ultimate med...
Toast Titanium goes way beyond the very basic burning in the Mac OS and iLife software, and sets the standard for burning CDs, DVDs, and now Blu-ray discs on the Mac. Create superior sounding audio... Read more
QuickBooks 2015 16.0.2.1422 R3 - Financi...
Save 20% on QuickBooks Pro for Mac today through this special discount link QuickBooks Pro 2013 helps you manage your business easily and efficiently. Organize your finances all in one place, track... Read more
Remotix 3.0.6 - Access all your computer...
Remotix is a fast and powerful application to easily access multiple Macs (and PCs) from your own Mac. Features: Complete Apple Screen Sharing support - including Mac OS X login, clipboard... Read more

Latest Forum Discussions

See All

The Hit List — Simply Powerful Tasks, To...
The Hit List — Simply Powerful Tasks, To-Dos, Projects, & Reminders 2.0 Device: iOS iPhone Category: Productivity Price: $9.99, Version: 2.0 (iTunes) Description: >> LAUNCH SPECIAL: The Hit List 2 for iPhone is ONLY $9.99... | Read more »
Mahjong Journey Review
Mahjong Journey Review By Jennifer Allen on December 18th, 2014 Our Rating: :: STEADY MATCHINGiPad Only App - Designed for the iPad Aimed at the more laid back gamer, Mahjong Journey isn’t for everyone, but those looking for some... | Read more »
Emoji Type - custom keyboard with predic...
Emoji Type - custom keyboard with predictive emojis 0.4.0 Device: iOS iPhone Category: Utilities Price: $.99, Version: 0.4.0 (iTunes) Description: Emoji Type is custom keyboard for iOS 8 that auto suggests emojis as you type. ABOUT... | Read more »
Game of the Year 2014 – 148Apps Staff Pi...
The end of 2014 is almost here, which can only mean one thing. Okay it can mean a lot of things, but in this specific context it means Game of the Year lists! Which is why the 148Apps staff have all picked their favorites from the past year. And why... | Read more »
UponPixels Review
UponPixels Review By Jennifer Allen on December 18th, 2014 Our Rating: :: CREATIVE TYPOGRAPHYUniversal App - Designed for iPhone and iPad Add cool typography and objects to your photos with the easy to use UponPixels.   | Read more »
The Vikings are Coming! CastleStorm’s Ne...
The Vikings are Coming! CastleStorm’s New Update Adds a Survival Mode Posted by Jessica Fisher on December 18th, 2014 [ permalink ] | Read more »
Duet Display (Productivity)
Duet Display 0.3.3 Device: iOS Universal Category: Productivity Price: $9.99, Version: 0.3.3 (iTunes) Description: Duet Display allows you to use your iPad or iPhone as an extra display. Developed by a team of ex-Apple engineers,... | Read more »
Dragon Quest III Review
Dragon Quest III Review By Jennifer Allen on December 18th, 2014 Our Rating: :: DATED BUT NOT WITHOUT MERITUniversal App - Designed for iPhone and iPad A walk down memory lane isn’t foolproof for Dragon Quest III, but it has its... | Read more »
8 KEMCO JRPGs Are Just $0.99 in Celebrat...
8 KEMCO JRPGs Are Just $0.99 in Celebration of the Holidays Posted by Jessica Fisher on December 18th, 2014 [ permalink ] KEMCO has announc | Read more »
Brothers in Arms 3: Sons of War Review
Brothers in Arms 3: Sons of War Review By Jennifer Allen on December 18th, 2014 Our Rating: :: FUN BUT PUSHYUniversal App - Designed for iPhone and iPad Brothers in Arms 3: Sons of War could be great fun, but its plethora of... | Read more »

Price Scanner via MacPrices.net

Save up to $400 on MacBooks with Apple Certif...
The Apple Store has Apple Certified Refurbished 2014 MacBook Pros and MacBook Airs available for up to $400 off the cost of new models. An Apple one-year warranty is included with each model, and... Read more
Save up to $300 on Macs, $30 on iPads with Ap...
Purchase a new Mac or iPad at The Apple Store for Education and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free,... Read more
iOS and Android OS Targeted by Man-in-the-Mid...
Cloud services security provider Akamai Technologies, Inc. has released, through the company’s Prolexic Security Engineering & Research Team (PLXsert), a new cybersecurity threat advisory. The... Read more
KMI MIDI K-Board Great Gift for Amateur &...
The K-Board is a MIDI Nano keyboard for music creation for iPad, Android, And computers; the easiest way to make music with iPads & Android tablets, and Mac, Windows, or Linux computers. Ultra-... Read more
Amazon offers 15-inch 2.2GHz Retina MacBook P...
 Amazon.com has the 15″ 2.2GHz Retina MacBook Pro on sale for $1699 including free shipping. Their price is $300 off MSRP. Stock is limited, so act now if you’re interested. Read more
Holiday sales continue: MacBook Pros for up t...
 B&H Photo has new MacBook Pros on sale for up to $300 off MSRP as part of their Holiday pricing. Shipping is free, and B&H charges NY sales tax only: - 15″ 2.2GHz Retina MacBook Pro: $1699... Read more
Holiday sale: Mac minis available for up to $...
 B&H Photo has new 2014 Mac minis on sale for up to $80 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 1.4GHz Mac mini: $459 $40 off MSRP - 2.6GHz Mac mini: $629 $70 off... Read more
Google Search App For iOS Gets A Major Makeov...
Google has given iOS users an early Christmas present with a substantial update of it’s not-very-often-upgraded Google Search app. Google Search has been my go-to tool for Web searches since it was... Read more
ShopKeep Apple Pay And Chip Card Reader Avail...
ShopKeep, a cloud-based technology provider to more than 10,000 small business owners to manage retail shops and restaurants with iPads, has released its new Apple Pay and chip card reader. This... Read more
Holiday sale! 27-inch 5K iMac for $2299, save...
 B&H Photo has the 27″ 3.5GHz 5K iMac in stock today and on sale for $2299 including free shipping plus NY sales tax only. Their price is $200 off MSRP, and it’s the lowest price available for... Read more

Jobs Board

*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
*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
*Apple* Retail - Multiple Positions (US) - A...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, 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
Project Manager / Business Analyst, WW *Appl...
…a senior project manager / business analyst to work within our Worldwide Apple Fulfillment Operations and the Business Process Re-engineering team. This role will work Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.