TweetFollow Us on Twitter

The Road to Code: Nobody's Perfect

Volume Number: 24
Issue Number: 11
Column Tag: The Road to Code

The Road to Code: Nobody's Perfect

Document Change Count and Undo

by Dave Dribinr

The last session of The Road to Code left us with a document-based application that used controllers with Cocoa bindings to hook the UI up to our model. Unfortunately, we have a lingering issue. Typical Mac OS X applications track changes to the user's documents and mark them as "dirty" when he makes changes. Thus, when the user tries to close the window or quit the application, you get a warning sheet about unsaved changes along with the chance to save the document. The red close button in the window bar shows documents that are dirty by placing a dot in the red button, as shown in Figure 1.


Figure 1: "Dirty" Dot in Close button

Typical Mac OS X applications also allow users to undo and redo their changes. Nobody is perfect, and everyone likes the ability to change his or her mistakes.

Unfortunately, our application does not behave this way. Users are able to change the width and height of existing rectangles or add and remove rectangles, and then close the window or quit the application with nary a "Do you want to save?" warning. On the plus side, Apple's document-based architecture provides hooks for us to manage the state of the document and provide undo support.

The Document Change Count

As a starting point, I have included, in Listing 1 and Listing 2, our MyDocument class from last month's article that used Cocoa bindings. Our changes will be centered on this class.

Listing 1: MyDocument.h

#import <Cocoa/Cocoa.h>
@interface MyDocument : NSDocument
{
    NSMutableArray * _rectangles;
}
@property (readwrite, copy) NSMutableArray * rectangles;
@end
Listing 2: MyDocument.m
#import "MyDocument.h"
#import "Rectangle.h"
@implementation MyDocument
@synthesize rectangles = _rectangles;
- (id)init
{
    self = [super init];
    if (self == nil)
        return nil;
    
    _rectangles = [[NSMutableArray alloc] init];
    
    return self;
}
- (NSString *)windowNibName
{
    // Override returning the nib file name of the document
    // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, 
    //you should remove this method and override -makeWindowControllers instead.
    return @"MyDocument";
}
- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
    // Add any code here that needs to be executed once the windowController has loaded the document's window.
}
- (NSData *)dataOfType:(NSString *)typeName
                 error:(NSError **)outError
{
    NSData * rectangleData =
    [NSKeyedArchiver archivedDataWithRootObject:_rectangles];
    return rectangleData;
}
- (BOOL)readFromData:(NSData *)data
              ofType:(NSString *)typeName
               error:(NSError **)outError
{
    _rectangles = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    return YES;
}
@end

Before we proceed, we should formally go over some terms. When a document can be safely closed without losing any changes it is called a clean document. Once the user starts editing a document, it must be saved or changes will be lost, and the document is called a dirty document. The NSDocument keeps track of whether the document is clean or dirty. The isDocumentEdited method returns YES if the document is dirty and NO if the document is clean. If you do nothing it always returns NO, which is why our document can be closed without prompting the user to save. To change the document state, you have to use the updateChangeCount: method. Thus, in your NSDocument subclass, you would have code that looks like this every time the user edits the document:

    [self updateChangeCount:NSChangeDone];

Why every time? Well, the document's edited state is actually implemented as a count called a change count. The document is clean when the change count is zero, and it's dirty when the change count is greater than zero. Why use a change count, rather than just a simple BOOL flag? This is useful once we talk about undo. For example, if the user makes five edits on a clean document, it becomes dirty. But if the user then runs undo five times, the document should be clean again. By implementing the document status as a change count, this is relatively easy: edits increment the change count and undos decrement the change count. We will be covering undo later in this article.

To increment the change count, we pass the NSChangeDone argument to the updateChangeCount: method, as I noted above. So to make sure our document is marked dirty, we just need to increment the change count any time the user makes an edit. Sounds easy, right? Well, it turns out to be relatively easy, but not trivial. Before we start updating the change count, let's list out all the possible ways a user can edit a rectangle document:

  • Add a new rectangle,
  • Remove an existing rectangle,
  • Change the width of an existing rectangle, or
  • Change the height of an existing rectangle.

Before using Cocoa bindings, these edits went through our document subclass, either through button actions or the table data source. Thus, without bindings, you would update the change count at these places in your document subclass. However, with Cocoa bindings, the array controller now handles all these edits, which actually complicates things a little.

Let's start with marking the document as dirty when adding and removing rectangles. Our MyDocument class has a rectangles property that is of type NSMutableArray. The array controller is bound to this property, so whenever rectangles are added and removed, it uses the setter of this property. Currently, the setter of this property is generated for use with the @synthesize keyword; however, if we write our own setter, we can insert code to update the change count:

- (void)setRectangles:(NSMutableArray *)rectangles
{
    if (rectangles == _rectangles)
        return;
    
    _rectangles = [rectangles copy];
    [self updateChangeCount:NSChangeDone];
}

The last line is the important line that marks the document as edited. If you run it now, the document will be marked as dirty as soon as you add a new rectangle.

However, the document will not be marked as dirty if you change the width or height of an existing rectangle. In order to see this incorrect behavior, you will have to save a document, which will mark the document as clean, and then change a rectangle.

To fix this, we need to handle the last two kinds of edits. When a width or height changes, it goes through the array controller, so you may think that subclassing NSArrayController would be the way to update the change count. It turns out that this is not the best way to implement the change count update, as NSArrayController is not meant to be subclassed for this purpose.

Key-Value Observing

Thankfully, there is another way. We briefly talked about key-value observing or KVO in a previous article, but now we are going to use it in earnest. KVO allows one object to observe changes of another object, thus we can use KVO to watch for changes to existing rectangles. KVO is based on key paths. An object starts observing a key path on a target object and gets notified whenever that key path changes. We need to setup MyDocument as an observer for the "width" and "height" key path of all rectangle objects in the _rectangles array. Let's start off by creating a method in MyDocument that sets itself up as an observer to a single rectangle:

- (void)startObservingRectangle:(Rectangle *)rectangle
{
    [rectangle addObserver:self
                forKeyPath:@"width"
                   options:0
                   context:&kRectangleEditContext];
    [rectangle addObserver:self
                forKeyPath:@"height"
                   options:0
                   context:&kRectangleEditContext];
}

Since each KVO registration is for a single key path, we have to observe each key path independently. We do not need any options, but we are using the context. The kRectangleEditContext is a static variable we need to set at the top of our source file:

static NSString * kRectangleEditContext = @"Rectangle Edit";

The context is a void * value and is passed in the KVO notifications so you discern between multiple observers. Remember that void * is a pointer to anything. It does not even have to be an Objective-C object, but we are using a pointer to an NSString * because it makes it easy to create a unique pointer value. We are using the same context because we want to perform the same action when either the "width" or "height" changes. Next, we have to implement the method that gets called when an observed key path changes:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == &kRectangleEditContext)
    {
        [self updateChangeCount:NSChangeDone];
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

All KVO notifications go through this one method. This is different than NSNotification-based notifications, where you can choose a method for notification delivery, and this is why the context is so important. Here, we compare the context against the address of kRectangleEditContext we used in to addObserver:.... If it is that context, we update the change count to mark the document as dirty. It is important to call your superclass's implementation if you do not handle the KVO notification in case it needs to handle its own KVO notifications.

We also need to create a method to unregister for KVO notifications, which we will use when a rectangle is removed. It's important to always remove yourself as an observer otherwise you may get runtime errors.

- (void)stopObservingRectangle:(Rectangle *)rectangle
{
    [rectangle removeObserver:self
                   forKeyPath:@"width"];
    [rectangle removeObserver:self
                   forKeyPath:@"height"];
}

We now have our KVO infrastructure in place, but we are not yet observing any rectangles. The place to do this is again in the setRectangles: setter. This method gets called with a whole new array of rectangles even if a single rectangle is added or removed. The behavior we want is to stop observing all rectangles in the old array and then start observing all rectangles in the new array. Here's what that looks like:

- (void)setRectangles:(NSMutableArray *)rectangles
{
    if (rectangles == _rectangles)
        return;
    
    for (Rectangle * rectagle in _rectangles)
        [self stopObservingRectangle:rectagle];
    
    _rectangles = [rectangles copy];
    for (Rectangle * rectagle in _rectangles)
        [self startObservingRectangle:rectagle];
    
    [self updateChangeCount:NSChangeDone];
}

So, before setting the _rectangles instance variable to the new array, we loop through all rectangles and stop observing them. And once we set the _rectangles instance variable, we start observing all these rectangles. Finally, we update the change count, as we did earlier, so that adding and removing of rectangles causes the change count to increment.

We have one final detail to cover in our program. If we open a saved document we still do not properly mark that the document is dirty after modifying a width or height. The problem is that our readFromData:... method is setting the _rectangles instance variable directly. This bypasses our setRectangles: accessor, so we never start to observe the saved rectangles. The solution is to use the setter by assigning via property dot notation:

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

Remember, using property dot notation is the same as calling the setter method. We're not quite done, yet. The setter marks the document as dirty, which means that a document will be immediately marked as dirty after opening. This is not desired, so the best way is to set the change count to zero after setting the rectangles property:

- (BOOL)readFromData:(NSData *)data
              ofType:(NSString *)typeName
               error:(NSError **)outError
{
    self.rectangles = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    [self updateChangeCount:NSChangeCleared];
    return YES;
}

The NSChangeCleared argument to updateChangeCount: causes the change count to be reset to zero, and thus marking the document as clean.

At this point, our application now properly tracks the document state. If we edit any width or height, it triggers a KVO notification, which in turn updates the change count. If we add or remove a rectangle, it also triggers an update to the change count. Note that saving a document or reverting to saved automatically sets the change count to zero and marks the document as clean.

Just because it works, however, does not mean we cannot improve things a bit. The one issue we are going to fix is the fact that the setRecangles: setter is used even if just a single rectangle is added or removed. This is fine if the number of rectangles in our array is small, but it can become a problem, as the array gets larger. While I'm not a big fan of optimizing before profiling (a technique to find slow portions of code), I'm going to show you how you could alleviate the problem if you need to.

Indexed Accessors

Instead of the array controller passing a whole new array every time a rectangle is added or deleted, wouldn't it be nice if it could tell you that a single rectangle has been added or deleted? It turns out that KVC already has the answer for us, and we just need to use it.

Most properties represent a single value, for example the width of a rectangle or the name of a person. Because it is a single value, it is also known as a to-one relationship. When a property represents multiple values, as is the case with the "rectangles" property of our MyDocument class, it is known as a to-many relationship. This is because for every one MyDocument object, there are many rectangles. For to-one relationships, the standard KVC method naming convention of -<key> and -set<Key> work just fine. For to-many relationships, there are optional methods you can implement for more efficient behavior called indexed accessors. On the getter side you have two new indexed getters:

-countOf<Key>
-objectIn<Key>AtIndex

These two methods allow you to get the number of objects in the to-many relationship or a single object without having to fetch the whole array. Here's how we would implement indexed getters for the "rectangles" property:

- (NSUInteger)countOfRectangles
{
    return [_rectangles count];
}
- (Rectangle *)objectInRectanglesAtIndex:(NSUInteger)index
{
    return [_rectangles objectAtIndex:index];
}

We implement these using the array's direct access API. While these are nice, it does not really help us with our problem. The are also two indexed accessor methods on the setter side:

-insertObject:in<Key>AtIndex:
-removeObjectFrom<Key>AtIndex:

Again, to implement these for the "rectangles" property, we would use the array API:

- (void)insertObject:(Rectangle *)rectangle
 inRectanglesAtIndex:(NSUInteger)index
{
    [_rectangles insertObject:rectangle atIndex:index];
}
- (void)removeObjectFromRectanglesAtIndex:(NSUInteger)index
{
    [_rectangles removeObjectAtIndex:index];
}

The NSArrayController is smart enough to use these methods to add or remove a single rectangle, if they exist. Of course, these methods still do not update the change count, but that's easy to add. We must additionally make sure to start and stop observing these rectangles, as well:

- (void)insertObject:(Rectangle *)rectangle
 inRectanglesAtIndex:(NSUInteger)index
{
    [self startObservingRectangle:rectangle];
    [_rectangles insertObject:rectangle atIndex:index];
    [self updateChangeCount:NSChangeDone];
}
- (void)removeObjectFromRectanglesAtIndex:(NSUInteger)index
{
    Rectangle * rectangleToRemove = [_rectangles objectAtIndex:index];
    [self stopObservingRectangle:rectangleToRemove];
    [_rectangles removeObjectAtIndex:index];
    [self updateChangeCount:NSChangeDone];
}

And voila! With two simple methods, we have optimized setters while keeping the ability to update the change count. We still want to keep the setRectangles: method for replacing all rectangles in one call. This is handy when opening a document, for example.

One nice thing about indexed accessors is that it allows you to implement to-many relationships without using an array internally. While it certainly is easiest to implement indexed accessors with an array, you are by no means required to do so.

Undo and Redo

While changing the document status is a good step towards making our application a standard OS X application, users also expect support for undo. Undo is handled in Cocoa by a class named NSUndoManager. Its purpose is to hold the list of undo and redo actions, and then perform these actions when the user invokes Undo or Redo from the Edit menu.

In order to understand how undo works, we need to dig a bit deeper into Objective-C method dispatching. A few articles back, we talked about selectors and how to use them to send messages. Say, for example, we have a Person class with a method to set the name:

- (void)setName:(NSString *)name;

You typically call this method using the square bracket syntax with which you are now very familiar:

    [person setName:@"Joe"];

Remember, the selector is the name of the method, including any colons, in this case "setName:". To call a method with its selector, you would use one of the performSelector: methods provided by NSObject. Thus, an alternate way to call the setName: method by using its selector is:

    [person performSelector:@selector(setName:)
                 withObject:@"Joe"];

This kind of method calling is called dynamic dispatch because the selector and arguments can be changed dynamically at runtime, instead of statically at compile time. As we also discussed earlier how Cocoa uses dynamic dispatch to invoke the actions of controls, such as when a user presses a button. But the nice thing about selectors and dynamic dispatch is that you have all the components you need to have a sort of freeze-dried method call. You can save the target, selector, and arguments in instance variables, for example, and then invoke the method later.

While the performSelector: methods provided by NSObject are very nice, they do have some limitations. For example, you can only pass up to two arguments and the arguments and return values must be Objective-C objects. You cannot pass primitive arguments or get primitive return values. For that, you have to bring out the big guns: NSInvocation.

NSInvocation is a class that encapsulates an entire method call, the target object, the selector, and all the arguments. It can also handle primitive types. The downside is that it is a bit of a pain to use. For example, let's say we wanted to set the width of a Rectangle using NSInvocation. Remember that properties also generate setter and getter methods, so we can use the setWidth: selector to set the width property. Here's the code:

    SEL selector = @selector(setWidth:);
    NSMethodSignature * signature =
        [rectangle methodSignatureForSelector:selector];
    NSInvocation * invocation =
        [NSInvocation invocationWithMethodSignature:signature];
    [invocation setTarget:rectangle];
    [invocation setSelector:selector];
    float newWidth = 30.0;
    [invocation setArgument:&newWidth atIndex:2];
    [invocation invoke];

Don't worry about fully understanding this code. Yes, the newWidth argument is actually the second argument. The first two arguments, 0 and 1, are always set to the target and selector, respectively. I just want to quickly introduce the NSInvocation object. If you want to full understand it, please consult the documentation.

The reason I bring up invocations is that NSUndoManager uses them under the hood to implement undo and redo actions. In fact, it keeps a stack of invocations for both the undo and redo actions. Say we're starting with a new, clean document and we add a new rectangle and change the width to 30, from the default of 15. Here is the list of actions the user performed, in order:

  • Add a rectangle at index zero.
  • Set the width of rectangle at index zero to 30.
In order to undo these operations, we must perform their inverse actions in reverse order:
  • Set width of rectangle at index zero to 15.
  • Remove rectangle at index zero.

How do you do you add actions to the undo manager in code? There are two ways. If you have a simple method with only a single argument that is an Objective-C type, you can use selectors:

    [undoManager registerUndoWithTarget:person
                               selector:@selector(setName:)
                                 object:@"Joe"];

However, if you need to use primitive types, then you need to use the NSInvocation variant. Thankfully, NSUndoManager allows you to do this without going through the whole rigmarole of creating an NSInvocation object. Here's how you would create an undo action to set the width of a rectangle to 15:

    [[undoManager prepareWithInvocationTarget:rectangle]
        setWidth:15];

Note that even though this looks like it is actually calling setWidth:15, it is only creating an invocation for this method. NSUndoManager is using some deep Objective-C magic to turn what looks like a real method call into an NSInvocation. We don't need to concern ourselves with how it does it (it uses forwardInvocation: under the covers if you do want to learn more), we just need to know that prepareWithInvocation: adds an invocation to the undo stack.

Adding Undo Support

Now that we know how to add actions to the undo manager, we can start modifying our code for undo support. We just need to modify every case where we insert or remove a rectangle or change the width or height of a rectangle. Thankfully, we already went through the process of identifying these locations for dirty document support, and this will help us add undo support. For example, to add an undo action when a rectangle is added, we can use our indexed accessor:

- (void)insertObject:(Rectangle *)rectangle
 inRectanglesAtIndex:(NSUInteger)index
{
    NSUndoManager * undoManager = [self undoManager];
    [[undoManager prepareWithInvocationTarget:self]
        removeObjectFromRectanglesAtIndex:index];
    if (![undoManager isUndoing])
        [undoManager setActionName:@"Add Rectangle"];
    
    [self startObservingRectangle:rectangle];
    [_rectangles insertObject:rectangle atIndex:index];
}

This requires a bit of explanation. First, we get an undo manager from self. NSDocument provides separate undo managers for each document, so each document has their own undo and redo stack.

The next interesting bit is that we can set the name of an action. This shows up in the Edit menu to provide the user with more context about a particular undo action. So instead of just showing Undo it will display Undo Add Rectangle. The only thing to be aware of is that you set the action name after adding an action to the undo manager, not before.

And finally, we no longer need to update the change count. The undo manager takes care of managing the change count for us.

We now need to add undo support to the other rectangle setter methods, in a similar fashion:

- (void)removeObjectFromRectanglesAtIndex:(NSUInteger)index
{
    Rectangle * rectangleToRemove = [_rectangles objectAtIndex:index];
    NSUndoManager * undoManager = [self undoManager];
    [[undoManager prepareWithInvocationTarget:self]
        insertObject:rectangleToRemove
 inRectanglesAtIndex:index];
    if (![undoManager isUndoing])
        [undoManager setActionName:@"Remove Rectangle"];
    
    [self stopObservingRectangle:rectangleToRemove];
    [_rectangles removeObjectAtIndex:index];
}
- (void)setRectangles:(NSMutableArray *)rectangles
{
    if (rectangles == _rectangles)
        return;
   
    for (Rectangle * rectagle in _rectangles)
        [self stopObservingRectangle:rectagle];
    
    NSUndoManager * undoManager = [self undoManager];
    [[undoManager prepareWithInvocationTarget:self]
        setRectangles:_rectangles];
    _rectangles = [rectangles copy];
    for (Rectangle * rectagle in _rectangles)
        [self startObservingRectangle:rectagle];
}

We basically replaced the change count calls with undo action registration. Earlier in this article, when we reset the change count when the document is loaded. Now, we want to do the same thing, but this time, we want to remove all actions from the undo stack:

- (BOOL)readFromData:(NSData *)data
              ofType:(NSString *)typeName
               error:(NSError **)outError
{
    self.rectangles = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    NSUndoManager * undoManager = [self undoManager];
    [undoManager removeAllActions];
    return YES;
}

The last detail is being able to undo individual edits. We used KVO to watch for changes to the width and height of each rectangle, and we can use KVO to support undo as well. The basic plan of attack is to use the KVO notification to add an undo action. The only problem is the KVO notification happens after the key path value has changed. If only we could get the previous value of the key path...

It turns out KVO has this capability. We do have to slightly change the way we start observing objects. Remember when we set the KVO options to 0? We are going to modify that to use the NSKeyValueObservingOptionOld option:

- (void)startObservingRectangle:(Rectangle *)rectangle
{
    [rectangle addObserver:self
                forKeyPath:@"width"
                   options:NSKeyValueObservingOptionOld
                   context:&kRectangleEditContext];
    [rectangle addObserver:self
                forKeyPath:@"height"
                   options:NSKeyValueObservingOptionOld
                   context:&kRectangleEditContext];
}

This tells KVO to supply the old value with the KVO notification. Our KVO notification callback method can now use that to add an undo action:

- (void)changeKeyPath:(NSString *)keyPath
             ofObject:(id)object
              toValue:(id)value
{
    [object setValue:value forKeyPath:keyPath];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == &kRectangleEditContext)
    {
        id oldValue = [change objectForKey:NSKeyValueChangeOldKey];
        
        NSUndoManager * undoManager = [self undoManager];
        [[undoManager prepareWithInvocationTarget:self]
            changeKeyPath:keyPath ofObject:object toValue:oldValue];
        [undoManager setActionName:@"Rectangle Edit"];
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

The change dictionary contains the old value. We use this, plus the keyPath and object to register an undo action. It uses a helper method that sets a value using KVC. KVO, like KVC, will automatically convert primitive number values into NSNumber objects. By using KVC, we also can share the implementation for both the width and height properties.

Conclusion

At this point, you should have an application with full dirty document and undo support. Your users will greatly appreciate the extra effort you've made for this bit of polish. If you are having trouble getting this all working, download the accompanying projects from the MacTech website.


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
$101.58
Apple Inc.
+0.72
MSFT
$46.52
Microsoft Corpora
-0.24
GOOG
$584.77
Google Inc.
+4.82

MacTech Search:
Community Search:

Software Updates via MacUpdate

iExplorer 3.5.0.0 - View and transfer al...
iExplorer is an iPhone browser for Mac lets you view the files on your iOS device. By using a drag and drop interface, you can quickly copy files and folders between your Mac and your iPhone or... Read more
BusyCal 2.6 - Powerful calendar app with...
BusyCal is an award-winning desktop calendar that combines personal productivity features for individuals with powerful calendar sharing capabilities for families and workgroups. BusyCal's unique... Read more
Apple iOS 8.0 - The latest version of Ap...
The latest version of iOS can be downloaded through iTunes. Apple iOS 8 comes with big updates to apps you use every day, like Messages and Photos. A whole new way to share content with your family.... Read more
Apple Digital Camera RAW Compatibility 5...
Apple Digital Camera RAW Compatibility update adds RAW image compatibility to Aperture 3 and iPhoto '11. For more information on supported RAW formats, see here.Version 5.07: Adds RAW camera... Read more
Transmit 4.4.7 - Excellent FTP/SFTP clie...
Transmit is an excellent FTP (file transfer protocol), SFTP, S3 (Amazon.com file hosting) and iDisk/WebDAV client that allows you to upload, download, and delete files over the internet. With the... Read more
Macgo Blu-ray Player 2.10.8.1715 - Blu-r...
Macgo Mac Blu-ray Player can bring you the most unforgettable Blu-ray experience on your Mac. Overview Macgo Mac Blu-ray Player can satisfy just about every need you could possibly have in a Blu-ray... Read more
Capture One Pro 8.0.0.433 - 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
Adobe Acrobat Pro 11.0.09 - Powerful PDF...
Adobe Acrobat allows users to communicate and collaborate more effectively and securely. Unify a wide range of content in a single organized PDF Portfolio. Collaborate through electronic document... Read more
Adobe Reader 11.0.09 - View PDF document...
Adobe Reader allows users to view PDF documents. You may not know what a PDF file is, but you've probably come across one at some point. PDF files are used by companies and even the IRS to... Read more
iFFmpeg 4.6.1 - Convert multimedia files...
iFFmpeg is a graphical front-end for FFmpeg, a command-line tool used to convert multimedia files between formats. The command line instructions can be very hard to master/understand, so iFFmpeg does... Read more

Latest Forum Discussions

See All

KuaiBoard (formerly QuickBoard) (Utilit...
KuaiBoard (formerly QuickBoard) 1.0 Device: iOS Universal Category: Utilities Price: $1.99, Version: 1.0 (iTunes) Description: KuaiBoard is currently 50% off for launch! Billing info. Signatures. Locations. KuaiBoard allows you to... | Read more »
Treasure Fetch - Adventure Time (Games)
Treasure Fetch - Adventure Time 1.0.1 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0.1 (iTunes) Description: Adventure Time Treasure Fetch is a fresh take on the classic Snake game! STRETCHY JAKE Snake your way... | Read more »
Light in the Dark (Games)
Light in the Dark 1.1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.1.0 (iTunes) Description: Be enlightened by this delightful puzzle game that has never before seen the light of day! "A physics and color-blending... | Read more »
Transmit iOS (Utilities)
Transmit iOS 1.0 Device: iOS Universal Category: Utilities Price: $9.99, Version: 1.0 (iTunes) Description: >> LAUNCH SPECIAL: Transmit iOS is ONLY $10 for a LIMITED TIME. << THE #1 FILE TRANSFER CLIENT FOR THE MAC NOW... | Read more »
Minuum - The Little Keyboard for Big Fin...
Minuum - The Little Keyboard for Big Fingers 1.0 Device: iOS iPhone Category: Utilities Price: $1.99, Version: 1.0 (iTunes) Description: Type faster, see more of your screen, and take control of autocorrect with Minuum: the little... | Read more »
TextExpander 3 + custom keyboard (Utili...
TextExpander 3 + custom keyboard 3.0 Device: iOS Universal Category: Utilities Price: $4.99, Version: 3.0 (iTunes) Description: | Read more »
Swype - Keyboard (Utilities)
Swype - Keyboard 1.0.1411 Device: iOS Universal Category: Utilities Price: $.99, Version: 1.0.1411 (iTunes) Description: Why type when you can Swype? One of the most requested apps for iPhone is finally here. "If Swype itself ever... | Read more »
Joinz (Games)
Joinz 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: Joinz is a clever block sliding puzzle game that is easy to play, nice to look at but tricky to master. You will be coming back again... | Read more »
Little World Escape Review
Little World Escape Review By Jordan Minor on September 17th, 2014 Our Rating: :: EARTHBOUNDUniversal App - Designed for iPhone and iPad Little World Escape draws players in with captivating concepts before pushing them away with... | Read more »
Light in the Dark Review
Light in the Dark Review By Nadia Oxford on September 17th, 2014 Our Rating: :: LIGHT 'EM UP UP UPUniversal App - Designed for iPhone and iPad Light in the Dark is an interesting and challenging puzzle game with some amusing bits... | Read more »

Price Scanner via MacPrices.net

Logitech Bluetooth Multi-Device Cross-Platfor...
Logitech has an enviable track record of making some of the best computer keyboards and mice. At least in my estimation, the best freestanding keyboards I’ve ever used have been Logitech units,... Read more
Roundup of Apple refurbished iPad Airs and iP...
Apple is offering Certified Refurbished iPad Airs for up to $140 off MSRP. Apple’s one-year warranty is included with each model, and shipping is free. Stock tends to come and go with some of these... Read more
Sprint offers 16GB iPad mini for $199.99 with...
Sprint is offering 1st generation 16GB iPad minis for $199.99 with a 2-year service agreement. Standard MSRP for this iPad is $429. Their price is the lowest available for this model. Read more
2.5GHz Mac mini remains on sale for $549, sav...
B&H Photo has the 2.5GHz Mac mini on sale for $549.99 including free shipping. That’s $50 off MSRP, and B&H will also include a free copy of Parallels Desktop software. NY sales tax only. Read more
Apple refurbished iMacs available for up to $...
The Apple Store has Apple Certified Refurbished iMacs available for up to $300 off the cost of new models. Apple’s one-year warranty is standard, and shipping is free. These are the best prices on... Read more
13″ 2.5GHz MacBook Pro offered for $100 off M...
B&H Photo has the 13″ 2.5GHz MacBook Pro on sale for $999.99 including free shipping plus NY sales tax only. Their price is $100 off MSRP. Read more
Free GIMP Professional Grade Graphics App Ver...
The latest 2.8.14 version of the oddly-named GIMP (acronym for: GNU Image Manipulation Program) open source, high-end image editing and creation alternative to Adobe’s Photoshop and refuge from... Read more
Apple Announces Record Pre-orders for iPhone...
Apple has released metrics showing a record number of first day pre-orders of iPhone 6 and iPhone 6 Plus, with over four million sold in the first 24 hours. Demand for the new iPhones exceeds the... Read more
10% off iPhone 6 and 6 Plus Otterbox cases
Get 10% off on popular Otterbox iPhone 6 and iPhone 6 Plus cases at MacMall through September 19th. Use code OTTERBOX10 to see the discount. Read more
15-inch MacBook Pros on sale for up to $125 o...
Amazon has the new 2014 15″ Retina MacBook Pros on sale for up to $125 off MSRP including free shipping: - 15″ 2.2GHz Retina MacBook Pro: $1899.99 save $100 - 15″ 2.5GHz Retina MacBook Pro: $2374... 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...
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...
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
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.