TweetFollow Us on Twitter

Introduction to Core Data, Part II

Volume Number: 21 (2005)
Issue Number: 9
Column Tag: Programming

Introduction to Core Data, Part II

Diving More Deeply into Apple's New Persistence Framework

by Jeff LaMarche

Introduction

In the first part of this article we built a simple, but relatively complete application for keeping track of a collection of books. We wrote very little code to create that application, and leveraged an amazing amount of Core Data's "for free" functionality. There will inevitably be times, as you write Core Data applications, however, where you need to be able to create, change, and delete data programmatically and there will also certainly be times when you need to do things in your application that Core Data simply doesn't do for you.

You might, for example, need to do conditional validation of a field, or provide a default value for a new instance based on other data in your application. In this month's article, we are going to combine the simple book-tracking application from the first Core Data article with our code from the NSXML article that ran in the June issue to create an application that will both track information about books, as well as look up book information based on ISBN using Amazon's web services.

In order to implement this functionality, we're going to need to be able to create new entity instances and populate the attributes of those instances programmatically. We're also going to need to subclass NSManagedObject to implement validation and defaulting behavior that is too complex to be accommodated by using only the various fields of the data modeler.

We'll be using our project from the first part of this article (which ran in the July issue) as our starting point for this month. Also, If you haven't already read it, you may want to review the article on NSXML from the June issue, since I will be using, but not explaining, some code we wrote in that article. You can download the project from the first part of this article at ftp://ftp.mactech.com/src/mactech/volume21_200521.07.sit and use that as a starting point if you wish. You'll also need the two categories from the NSXML project in order to compile some code in this month's article, so you'll probably want to grab it at ftp://ftp.mactech.com/src/mactech/volume21_2005/21.06.sit if you plan on trying out this month's code.

Start Me Up

Open up the MTCoreData project from last month in Xcode. Single click on MTCoreData_DataModel.xcdatamodel so that the data model editor is showing. If the editor doesn't show up when you single-click the data model icon, select Zoom Editor In from the View menu, or press the Editor button on Xcode's toolbar.

Single-click on the Book entity in the Entity pane (the top left pane in the data model editor). Once you do that, a list of the book's attributes should appear in the top middle pane. There are two attributes to which I want to draw your attention. These attributes are dateRead and url. Now, wouldn't it be nice if we could default the dateRead field to the date on which the book was entered into the database, or find a way to validate that url is actually a valid internet URL? There's no way to do either of these things using just the entity or attribute inspectors available in the data modeler, but they can be done. In fact, we're going to do both of those things in this article.

If you look in the top right-most pane of the editor, you'll notice that the Book entity currently has specified a class of NSManagedObject. Go ahead and change that to MTBookEntity. What we've just done is specified a different class to represent the Book entity. At the moment, the class we specified doesn't exist, but we're going to create it.


Figure 1. Creating the files for MTBookEntity

Since NSManagedObject already does most of what we need done, our new class is going to be a subclass of it. Press ?N or select New File... from the File Menu. Select Objective-C Class from the New File Assistant and name the new file MTBookEntity.m. Before hitting return, make sure that Also Create "MTBookEntity.h" is selected (figure 1). After you hit return, you'll have two new files in your project. Single-click first on MTBookEntity.h, so that we can change the superclass from NSObject to NSManagedObject. This is absolutely necessary; although the modeler will let you specify any class, at runtime your application is only going to work if the class you specified responds to all the methods that NSManagedObject responds to.

MTBookEntity.h
The only change at this point is to make NSManagedObject rather than NSObject the superclass.

#import <Cocoa/Cocoa.h>

@interface MTBookEntity : NSManagedObject 
{

}
@end

Implementing Custom Default Behavior

You'll notice that we haven't added any new methods or instance variables at this point. NSManagedObject already provides us with a mechanism for validating and defaulting values, as well as methods to allow getting and setting attributes using Key Value Coding or KVC.

Switch now to MTBookEntity.m so we can implement the defaulting behavior for the dateRead attribute. There is a method in NSManagedObject that is specifically designed to be overridden by subclasses in order to set default attribute values. That method is called awakeFromInsert. To set the default value of dateRead to the current date, all we have to do is override awakeFromInsert, and set the attribute using key value coding, like so:

MTBookEntity.m
This method is called when an entity instance is first created. It is where 
customized default values for attributes should be set.

#import "MTBookEntity.h"

@implementation MTBookEntity
- (void) awakeFromInsert
{
	[self setValue:[NSCalendarDate date] forKey:@"dateRead"];
}
@end

Yep, that's all there is to it. We don't even need to call [super awakeFromInsert]; we just implement the method if we need it and do any custom defaulting that we need to do. Notice that we didn't use a mutator to set the value, but rather used setValue:forKey: and passed in the name of the attribute we're setting as the key value.

Custom Attribute Validation

As you just saw, adding custom defaulting to a custom managed object couldn't be easier. Custom validation is not quite as straightforward. The obvious and, unfortunately, wrong way to do it would be to subclass NSManagedObject's validateValue:forKey:error: and implement your validation there. Apple strongly suggests that developers implement custom validation methods and leave validateValue:forKey:error: alone. Your custom methods will be called instead of validateValue:forKey:error: if your method name conforms to the naming convention validate:error:, where is replaced with the name of the attribute for which you wish to implement validation (i.e. validateFoo:Error:). Implementing the method is relatively simple; you just return YES if the value is okay to be saved, and NO if it isn't. If you return NO, you should also allocate an NSError with more detailed information about why validation failed. Let's create a custom validation method for url, shall we?

Custom validation methods (and, as we'll see later, custom accessors and mutators) must be declared in your subclass' header file because they do not exist in the superclass (NSManagedObject). Therefore, we have to add a method declaration to MTBookEntity.h:

MTBookEntity.h
Add this declaration just before @end

-(BOOL)validateUrl:(NSString **)urlString 
   error:(NSError **)error;

After creating the method declaration, we need to switch to the implementation file and write the actual validation. There is a bit of a gotcha here. See those double astericks (**) on the method's parameters? That's not a typo; both parameters for custom validation methods are passed as pointers to pointers (sometimes called a handle), so you have to de-reference them before trying to access them as Objective-C objects.

MTBookEntity.m

-(BOOL)validateUrl:(NSString **)urlString 
   error:(NSError **)error
{  

   // Create a local de-referenced variable to make code more readable
   // You can skip this and just use *urlstring anywhere I use val
   // if you prefer not to allocate a stack variable unnecessarily
	
NSString *val = *urlString;

   // Not a required attribute, so bail without validating if empty or nil
	
if (val == nil || [val length] == 0)
      return YES;
    
   // Create an NSURL. If unable to instantiate, populate error and return NO
	
NSURL *url = [NSURL URLWithString:val];
   if (url == nil)
   {

      // If we create a dictionary containing a string using one of the 
      // following keys:
      //    NSLocalizedDescriptionKey
      //    NSLocalizedFailureReasonErrorKey
      // that message will be displayed when validation failed.
      // Otherwise, it will use the error code and error domain
      // to determine what to display to the user. Since we don't
      // have an error code that corresponds to our error, we'll
      // do it this way and pass a generic -1 error code

   NSDictionary *userInfoDict = [NSDictionary 
         dictionaryWithObject:@"Not a valid URL"
         forKey:NSLocalizedDescriptionKey];

      // There are a number of error domains that correspond to 
      // where the error was originally generated. Most of the 
      // time you'll use the first
      //    NSCocoaErrorDomain
      //       NSPOSIXErrorDomain
      //       NSOSStatusErrorDomain
      //    NSMachErrorDomain

   *error = [[[NSError alloc] initWithDomain:
         NSCocoaErrorDomain code:-1 userInfo:userInfoDict] 
         autorelease];
      return NO;
   }
   return YES;
}

Other than the fact that we're being passed a handle rather than a pointer to an Objective-C object, this is a fairly straight forward chunk of code. We return YES if the value can be turned into an NSURL, NO if it can't. If the URL doesn't validate, we allocate an NSError and populate it with an error domain, an error code, and a dictionary containing a string that explains why validation failed.

Please note that you should avoid making changes to the object being passed in for validation. Since you're given a handle to it, you actually can change the actual object that's stored in Core Data's object graph, but doing so could potentially cause data consistency problems. Although it does seems like they wouldn't pass you the data in this manner unless they expected you to sometimes change it, Apple's documentation is very clear in stating that you should not. So don't, okay?

When Core Data goes to validate an attribute, it will first look for a custom validation method like the one we just created. If such a method exists, it gets called. If no such method exists, Core Data will then call the generic validateValue:forKey:error:. In that situation, we let our superclass handle the validation.

Custom Accessors & Mutators

Before we dive into creating, modifying, and deleting objects programmatically, let's look at implementing custom accessors and mutators (the methods used to get and set the value of instance variables) for our subclass. Now, this is a purely optional step: You never need to implement accessors or mutators for subclasses of NSManagedObject. The standard way of accessing attributes of an entity is to use Key Value Coding, like so:

name = [entity valueForKey:@"name"];

This method of getting data from an entity works perfectly well, regardless of whether you are using NSManagedObject or a custom subclasss. When you do subclass NSManagedObject, however, you have the option of implementing custom accessors and mutators so that your subclass can be use in a more intuitive fashion, like this:

name = [entity name];

Doing this generally makes for code that's a touch shorter, and which most people find to be a bit more readable. The tradeoff, of course, is that you have to actually do the work to implement these custom methods. Now choosing to implement them is not an all-or-nothing proposition. If you want to implement accessors and mutators just for the attributes you use most often rather than for all of the entity's attributes, that's perfectly acceptable. In the interest of space, we'll implement accessors and mutators for just one attribute - title - to show how it's done.

Just as with custom validation methods, you should declare custom accessors and mutators in your header file.

MTBookEntity.h
Add the following declarations before the @end directive

-(NSString *)title;
-(void)setTitle(NSString *)newTitle;

To implement these methods, we use NSManagedObject's key-kalue methods to get and set the attribute values, but there's a little more to it than that because we have to let Core Data know that we are accessing data that it manages. Core Data is very savvy about keeping its data context consistent even when it's being accessed from different places in your application, but we have to help it do its job right by telling it when we're going to start, and then again when we're done accessing an attribute.

Outside of your NSManagedObject subclass, it is generally not necessary or even advisable to do this, but you must do it here because we are directly accessing the data primitives. To implement these methods correctly, we have to access and set the primitive values using primitiveValueForKey: and setPrimitiveValue:forKey:. These two methods function identically to the standard valueForKey: and setValue:forKey: methods with which you are probably already familiar, but these two must be used when creating custom accessors and mutators, and no place else. Here are our implemented custom methods:

MTBookEntity.m

- (NSString *)title
{
   [self willAccessValueForKey:@"title"];
   id title = [self primitiveValueForKey:@"title"];
   [self didAccessValueForKey:@"title"];
   return title;
}
- (void)setTitle:(NSString *)newTitle
{
   [self willChangeValueForKey:@"title"];
   [self setPrimitiveValue:newTitle forKey:@"title"];
   [self didChangeValueForKey:@"title"];
}

At this point, you should be able to run the application, and it will work exactly as before, except that the dateRead field will default to today's date, and only valid URLs will be accepted in the url field.

Virtual Accessors

Another handy thing that you can do when subclassing NSManagedObject is to create virtual accessors. A virtual accessor is a method that functions just like an accessor, meaning you can bind interface elements to it in Interface Builder. What's being accessed is not an actual entity, however. Typically, you would do this with data calculated from actual attributes.

In our case, let's say we wanted to display the title of our books in the left-hand column just as we do now, but we wanted to include the year the book was released in parenthesis after the title. We don't want to add a column to the table, but just show it as a single column as it is now. Obviously, we want to keep these two data fields separate in the data context. This is an ideal place for a virtual accessor. Go ahead and declare a new accessor method called displayTitle: that returns an NSString.

MTCoreAppDelegate.h
Add this declaration before the @end directive

-(NSString *)displayTitle;

Now implement this method just as we would a "real" accessor.

MTCoreAppDelegate.m
-(NSString *)displayTitle
{
   NSString *displayTitle = nil;
   [self willAccessValueForKey:@"title"];
   [self willAccessValueForKey:@"dateReleased"];
   NSString *title = [self primitiveValueForKey:@"title"];
   NSDate *dateReleased = [self 
      primitiveValueForKey:@"dateReleased"];
   if (dateReleased)
      displayTitle = [NSString stringWithFormat:@"%@ (%@)", 
         title, [dateReleased 
         descriptionWithCalendarFormat:@"%Y" timeZone:nil 
         locale:nil]];
   [self didAccessValueForKey:@"title"];
   [self didAccessValueForKey:@"dateReleased"];
   return (displayTitle == nil) ? title : displayTitle; 
}

Since we're accessing multiple attributes, we have to tell Core Data about every attribute that we're using, and then again tell it when we're done with them. If there is no dateReleased field, we return just the title by itself, otherwise return the title followed by the year from the date in parenthesis.

Okay, now that we have our very own virtual accessor, what do we do with it? Well, fire up Interface Builder by double-clicking MainMenu.nib, and I'll show you. Once Interface Builder is launched, double-click on the table on the left hand side of your application's main window--the one that displays the list of books--and then single-click the column header. Press ?4 to bring up the bindings inspector, and expand the value binding. The current Model Key Path, you'll see, is set to title. Change that to read displayTitle, which will point it to our virtual accessor, and then save.

You can go ahead back to Xcode now and run the program if you want. You'll see that instead of just the titles in the left hand column, there will now be the titles followed by the year the book was published in parenthesis. Since the interface and data storage in Core Data applications are totally independent of one another, the table column doesn't know and doesn't care that displayTitle is not a real attribute.

Creating and Editing Core Data Entities

You may recall that we put a button on the interface last month, but didn't put any code behind that button. Well, it's now time for the main event; let's put some code behind it. The first thing we need to do is to declare a new IBOutlet method so that we have something to which we can bind the Lookup button.

MTCoreAppDelegate.h
Add a new method declaration to the existing application delegate header.

- (IBAction)doLookup:(id)sender;

And, of course, we need to implement this new method. The comments explain what's going on.

MTCoreAppDelegate.m
Here is the implementation of our new action method. This method retrieves the 
information about a book from Amazon based on the entered ISBN value, creates a new 
MTBookEntity instance, and then sets the various fields from the retrieved data.

- (IBAction)doLookup:(id)sender
{
 
   // Grab the currently selected item in the left-hand table 
   // - the one that's currently displayed
	
MTBookEntity *book = [[books selectedObjects] 
      objectAtIndex:0];

   // Retrieve the ISBN number typed in by the user

   NSString *isbn = [[book valueForKey:@"isbn"] 
      removeCharacters:@" -_/\\*."];
      
   // Use the ISBN the user typed in to create an Amazon URL
	
   NSString *urlBase = @"http://xml.amazon.com/onca/xml3?t=
                       1&dev-t=%@&AsinSearch=%@&type=heavy&f=xml";
   NSString	*urlString = [NSString stringWithFormat:urlBase, 
      AMAZON_DEV_TOKEN, isbn];
   NSURL *theURL = [NSURL URLWithString:urlString];
   NSError *err=nil;
    
   // Initialize our document with the XML data in our URL
	
   NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] 
      initWithContentsOfURL:theURL options:nil error:&err];
    
   // Get a reference to the root node
	
   NSXMLNode *rootNode = [xmlDoc rootElement];
    
   // In case of an error, Amazon includes a node called "ErrorMsg", its 
   // presence tells us that an error happened, so we check for it
	
   NSXMLNode *errorNode = [rootNode childNamed:@"ErrorMsg"];
    
   if (rootNode == nil || errorNode != nil)
   {
		
      // Nothing retrieved or error, throw up alert
		

NSRunAlertPanel(@"Error",@"Error retrieving XML data 
         about this book. Are you sure that's a valid 
         ISBN and you're connected to the 
         internet?",@"OK",nil,nil);
      return;
   }
   else
   {
	
   // Grab the details node
		
      NSXMLElement *detailsNode = [rootNode 
         childNamed:@"Details"];
	
      // Here's how we set a value using a custom mutator
		
NSString *bookTitle = [[detailsNode childNamed:
         @"ProductName"] stringValue];		
      [book setTitle:bookTitle];
	
      // Setting value using KVC
		
      NSString *publisher = [[detailsNode childNamed:
         @"Manufacturer"] stringValue];
      [book setValue:publisher forKey:@"publisher"];

      [book setValue:[[detailsNode childNamed:@"Asin"] 
         stringValue] forKey:@"isbn"];

      // We have to convert this number which is stored as a
      // string into an NSNumber before setting value. Core
      // Data number fields will not accept NSStrings even
      // if they contain a valid number
		
      NSNumber *rank = [NSNumber numberWithInt:[[[detailsNode 
         childNamed:@"SalesRank"] stringValue] intValue]];
      [book setValue:rank forKey:@"salesRank"];

      [book setValue:[[detailsNode attributeForName:@"url"] 
         stringValue] forKey:@"url"];
	
      // Get an enumerator that contains all the authors 
      // listed in the XML 

      NSXMLNode *authorsNode = [detailsNode 
         childNamed:@"Authors"];
      NSEnumerator *e = [[authorsNode childrenAsStrings] 
         objectEnumerator];
      NSString *oneAuthor;
  
      // Get the Mutable Set that corresponds to the authors 
      // relationship - this will allow us to add Author entities 
      // to this Book entity

      NSMutableSet *authorSet = [book mutableSetValueForKey:
         @"authors"];

      // Loop through retrieved authors enumerator

      while (oneAuthor = [e nextObject]) 
      {

         // Create and insert a new Author entity for each author found

         NSManagedObject *author = [NSEntityDescription 
            insertNewObjectForEntityForName:@"Author" 
            inManagedObjectContext:[self 
            managedObjectContext]];
            
         // Set the author's name attribute

         [author setValue:oneAuthor forKey:@"name"];

         // Add the new author to the Book entity's authors relationship

         [authorSet addObject:author];

         // Note: Core Data automatically populates the Inverse relationship
      }

      // Get the image data from URL then store it

      NSURL *imageURL = [NSURL URLWithString:[[detailsNode 
         childNamed:@"ImageUrlLarge"] stringValue]];
      [book setValue:[NSData dataWithContentsOfURL:imageURL] 
         forKey:@"coverImage"];

   }
}

That listing may look a little daunting, but don't be scared off by it; Core Data entities are actually very easy to work with. You use valueForKey: and setValue:forKey: to get and set the values for any given instance or, if you choose to implement them, you can call custom accessors and mutators instead. To add an existing entity to the relationship of another entity, you use mutableSetValueForKey: to retrieve the NSMutableSet that represents that relationship. Then when you add or delete items from the returned NSMutableSet instance, what you are actually doing is adding or deleting them from the entity's relationship.

Creating new objects, as you saw, is done by calling one of NSEntityDescription's class methods called insertNewObjectForEntityForName:inManagedObjectContext:, supply the name of the type of entity you want to create along with the context in which you want it created, and it will create the instance and return a reference to it.

Now that we have our code in place, go to Interface Builder and make sure that the Lookup button's target outlet is bound to the doLookup: method, and then go try it out.

Deleting Objects

One useful task that we didn't undertake in the code above was deleting an Entity. In our application, the only place where we need to delete entities is really better handled as it currently is: by using NSArrayController's remove: outlet. Just for grins and giggles, let's take a quick look at how we would delete a book programmatically if we needed to. This is really, insanely hard, so if you don't grasp it at first, it's okay. Just take a few deep breaths and re-read the code sample a few times until it makes sense. I'm confident you can grasp it if you try hard enough.

Deleting a Core Data Entity
[[self managedObjectContext] deleteObject:book];

Still with me? Are you sure? Great!


Figure 2. The final application in action.

This is the End

That's all there is for this month. We looked at subclassing NSManagedObject in order to implement conditional defaulting and validation for our entity. We also took a look at how we create, edit, and delete managed objects programmatically. Core Data is really an amazing technology and a huge productivity booster for Mac application developers; these two articles have only scratched the surface of what it can do for you. They should, however, have you enough of a foundation to get in there and start making Core Data applications that really dance the Fandango. Now, what are you waiting for? Go to it!


Jeff LaMarche wrote his first line of code in Applesoft Basic on a Bell & Howell Apple //e in 1980 and he's owned at least one Apple computer at all times since. Though he currently makes his living consulting in the Mac-unfriendly world of "Enterprise" software, his Macs remain his first and greatest computer love. You can reach him at jeff_lamarche@mac.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Opera 48.0.2685.50 - High-performance We...
Opera is a fast and secure browser trusted by millions of users. With the intuitive interface, Speed Dial and visual bookmarks for organizing favorite sites, news feature with fresh, relevant content... Read more
Tor Browser Bundle 7.0.7 - Anonymize Web...
The Tor Browser Bundle is an easy-to-use portable package of Tor, Vidalia, Torbutton, and a Firefox fork preconfigured to work together out of the box. It contains a modified copy of Firefox that... Read more
FotoMagico 5.5 - Powerful slideshow crea...
FotoMagico lets you create professional slideshows from your photos and music with just a few, simple mouse clicks. It sports a very clean and intuitive yet powerful user interface. High image... Read more
Adobe Audition CC 2018 11.0.0 - Professi...
Audition CC 2018 is available as part of Adobe Creative Cloud for as little as $19.99/month (or $9.99/month if you're a previous Audition customer). Adobe Audition CC 2018 empowers you to create and... Read more
Alfred 3.5.1 - Quick launcher for apps a...
Alfred is an award-winning productivity application for OS X. Alfred saves you time when you search for files online or on your Mac. Be more productive with hotkeys, keywords, and file actions at... Read more
AirRadar 4.0 - $9.95
With AirRadar, scanning for wireless networks is now easier and more personalized! It allows you to scan for open networks and tag them as favourites or filter them out. View detailed network... Read more
DEVONthink Pro 2.9.16 - Knowledge base,...
Save 10% with our exclusive coupon code: MACUPDATE10 DEVONthink Pro is your essential assistant for today's world, where almost everything is digital. From shopping receipts to important research... Read more
ForkLift 3.0.8 Beta - Powerful file mana...
ForkLift is a powerful file manager and ferociously fast FTP client clothed in a clean and versatile UI that offers the combination of absolute simplicity and raw power expected from a well-executed... Read more
Data Rescue 5.0.1 - Powerful hard drive...
Data Rescue’s new and improved features let you scan, search, and recover your files faster than ever before. We have modernized the file-preview capabilities, added new files types to the recovery... Read more
Dropbox 37.4.29 - Cloud backup and synch...
Dropbox is an application that creates a special Finder folder that automatically syncs online and between your computers. It allows you to both backup files and keep them up-to-date between systems... Read more

Halcyon 6: Starbase Commander (Games)
Halcyon 6: Starbase Commander 1.4.2.0 Device: iOS Universal Category: Games Price: $6.99, Version: 1.4.2.0 (iTunes) Description: An epic space strategy RPG with base building, deep tactical combat, crew management, alien diplomacy,... | Read more »
Legacy of Discord celebrates its 1 year...
It’s been a thrilling first year for fans of Legacy of Discord, the stunning PvP dungeon-crawling ARPG from YOOZOO Games, and now it’s time to celebrate the game’s first anniversary. The developers are amping up the festivities with some exciting... | Read more »
3 reasons to play Thunder Armada - the n...
The bygone days of the Battleship board game might have past, but naval combat simulators still find an audience on mobile. Thunder Armada is Chinese developer Chyogames latest entry into the genre, drawing inspiration from the explosive exchanges... | Read more »
Experience a full 3D fantasy MMORPG, as...
Those hoping to sink their teeth into a meaty hack and slash RPG that encourages you to fight with others might want to check out EZFun’s new Eternity Guardians. Available to download for iOS and Android, Eternity Guardians is an MMORPG that lets... | Read more »
Warhammer Quest 2 (Games)
Warhammer Quest 2 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: Dungeon adventures in the Warhammer World are back! | Read more »
4 of the best Halloween updates for mobi...
Halloween is certainly one of our favorite times for mobile game updates. Many popular titles celebrate this spooky season with fun festivities that can stretch from one week to even the whole month. As we draw closer and closer to Halloween, we'... | Read more »
Fire Rides guide - how to swing to succe...
It's another day, which means another Voodoo game has come to glue our hands to our mobile phones. Yes, it's been an especially prolific month for this particular mobile publisher, but we're certainly not complaining. Fire Rides is yet another... | Read more »
Time Recoil (Games)
Time Recoil 1.0.1 Device: iOS Universal Category: Games Price: $3.99, Version: 1.0.1 (iTunes) Description: Time Recoil is a top-down shooter where you kill to slow time, dominate slow motion gunfights, and trigger devastating special... | Read more »
Campfire Cooking (Games)
Campfire Cooking 1.0 Device: iOS Universal Category: Games Price: $3.99, Version: 1.0 (iTunes) Description: | Read more »
Returner 77 (Games)
Returner 77 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: Returner 77 is a cinematic space mystery puzzle game. You are in a giant alien spaceship hovering above Earth, after everything... | Read more »

Price Scanner via MacPrices.net

Apple restocks full line of refurbished 13″ M...
Apple has restocked a full line of Apple Certified Refurbished 2017 13″ MacBook Pros for $200-$300 off MSRP. A standard Apple one-year warranty is included with each MacBook, and shipping is free.... Read more
13″ 3.1GHz/256GB MacBook Pro on sale for $167...
Amazon has the 2017 13″ 3.1GHz/256GB Space Gray MacBook Pro on sale today for $121 off MSRP including free shipping: – 13″ 3.1GHz/256GB Space Gray MacBook Pro (MPXV2LL/A): $1678 $121 off MSRP Keep an... Read more
13″ MacBook Pros on sale for up to $120 off M...
B&H Photo has 2017 13″ MacBook Pros in stock today and on sale for up to $120 off MSRP, each including free shipping plus NY & NJ sales tax only: – 13-inch 2.3GHz/128GB Space Gray MacBook... Read more
15″ MacBook Pros on sale for up to $200 off M...
B&H Photo has 15″ MacBook Pros on sale for up to $200 off MSRP. Shipping is free, and B&H charges sales tax in NY & NJ only: – 15″ 2.8GHz MacBook Pro Space Gray (MPTR2LL/A): $2249, $150... Read more
Roundup of Apple Certified Refurbished iMacs,...
Apple has a full line of Certified Refurbished 2017 21″ and 27″ iMacs available starting at $1019 and ranging up to $350 off original MSRP. Apple’s one-year warranty is standard, and shipping is free... Read more
Sale! 27″ 3.8GHz 5K iMac for $2098, save $201...
Amazon has the 27″ 3.8GHz 5K iMac (MNED2LL/A) on sale today for $2098 including free shipping. Their price is $201 off MSRP, and it’s the lowest price available for this model (Apple’s $1949... Read more
Sale! 10″ Apple WiFi iPad Pros for up to $100...
B&H Photo has 10.5″ WiFi iPad Pros in stock today and on sale for $50-$100 off MSRP. Each iPad includes free shipping, and B&H charges sales tax in NY & NJ only: – 10.5″ 64GB iPad Pro: $... Read more
Apple iMacs on sale for up to $130 off MSRP w...
B&H Photo has 21-inch and 27-inch iMacs in stock and on sale for up to $130 off MSRP including free shipping. B&H charges sales tax in NY & NJ only: – 27″ 3.8GHz iMac (MNED2LL/A): $2179 $... Read more
2017 3.5GHz 6-Core Mac Pro on sale for $2799,...
B&H Photo has the 2017 3.5GHz 6-Core Mac Pro (MD878LL/A) on sale today for $2799 including free shipping plus NY & NJ sales tax only . Their price is $200 off MSRP. Read more
12″ 1.2GHz Space Gray MacBook on sale for $11...
Amazon has the 2017 12″ 1.2GHz Space Gray Retina MacBook on sale for $100 off MSRP. Shipping is free: 12″ 1.2GHz Space Gray MacBook: $1199.99 $100 off MSRP Read more

Jobs Board

Frameworks Engineer, *Apple* Watch - Apple...
Job Summary Join the team that is shaping the future of software development for Apple Watch! As a software engineer on the Apple Watch Frameworks team you will Read more
*Apple* News Product Marketing Mgr., Publish...
Job Summary The Apple News Product Marketing Manager will work closely with a cross-functional group to assist in defining and marketing new features and services. Read more
Fraud Analyst, *Apple* Advertising Platform...
Job Summary Apple Ad Platforms has an opportunity to redefine advertising on mobile devices. Apple reaches hundreds of millions of iPhone, iPod touch, and iPad Read more
*Apple* Information Security - Security Data...
Job Summary This role is responsible for helping to strengthen Apple 's information security posture through the identification and curation of security event data. Read more
Lead *Apple* Solution Consultant - Apple In...
…develop a team of diverse partner employees focusing on excellence to deliver the Apple story. Even when you're not present, you will maintain a consistent influence Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.