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/.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

FileZilla 3.51.0 - Fast and reliable FTP...
FileZilla (ported from Windows) is a fast and reliable FTP client and server with lots of useful features and an intuitive interface. Version 3.51.0: Bugfixes and minor changes: Fixed import of... Read more
KeyCue 9.8 - Displays all menu shortcut...
KeyCue has always been a handy tool for learning and remembering keyboard shortcuts. With a simple keystroke or click, KeyCue displays a table with all available keyboard shortcuts, system-wide... Read more
AppCleaner 3.5.1 - Uninstall your apps e...
AppCleaner allows you to uninstall your apps easily. It searches the files created by the applications and you can delete them quickly. Version 3.5.1: Fixed a code-signing issue causing AppCleaner... Read more
A Better Finder Attributes 7.03 - Change...
A Better Finder Attributes allows you to change JPEG & RAW shooting dates, JPEG EXIF meta-data tags, file creation & modification dates, file flags and deal with invisible files. Correct EXIF... Read more
Postbox 7.0.33 - Powerful and flexible e...
Postbox is a desktop feature-stuffed email client, news application, and feed reader that helps you manage your work life and increase productivity. Now you can organize all your email accounts in... Read more
Adobe InCopy 16.0 - Create streamlined e...
InCopy is available as part of Adobe Creative Cloud for $52.99/month (or $4.99/month for InCopy app only). Adobe InCopy, ideal for large team projects involving both written copy and design work,... Read more
Steam 2.0 - Multiplayer and communicatio...
Steam is a digital distribution, digital rights management, multiplayer and communications platform developed by Valve Corporation. It is used to distribute a large number of games and related media... Read more
Adobe Lightroom Classic 10.0 - Import, d...
You can download Lightroom for Mac as a part of Creative Cloud for only $9.99/month with Photoshop, included as part of the photography package. The latest version of Lightroom gives you all of the... Read more
Adobe InDesign 16.0 - Professional print...
InDesign is available as part of Adobe Creative Cloud for as little as $20.99/month (or $9.99/month if you're a previous InDesign customer). Adobe InDesign is part of Creative Cloud. That means you... Read more
Adobe After Effects 17.5 - Create profes...
After Effects is available as part of Adobe Creative Cloud for $52.99/month (or $20.99/month for a single app license). The new, more connected After Effects can make the impossible possible. Get... Read more

Latest Forum Discussions

See All

Genshin Impact Currency Guide - What...
Genshin Impact is great fun, but make no mistake: this is a gacha game. It is designed specifically to suck away time and money from you, and one of the ways the game does this is by offering a drip-feed of currencies you will feel compelled to... | Read more »
XCOM 2 Collection on iOS now available f...
The XCOM 2 Collection, which was recently announced to be coming to iOS in November, is now available to pre-order on the App Store. [Read more] | Read more »
Presidents Run has returned for the 2020...
IKIN's popular endless runner Presidents Run has returned to iOS and Android just in time for the 2020 election season. It will see players choosing their favourite candidate and guiding them on a literal run for presidency to gather as many votes... | Read more »
New update for Cookies Must Die adds new...
A new update for Rebel Twins’ platformer shooter Cookies Must Die is coming out this week. The update adds quite a bit to the game, including new levels and characters to play around with. [Read more] | Read more »
Genshin Impact Guide - How to Beat Pyro...
The end game of Genshin Impact largely revolves around spending resin to take on world bosses and clear domain challenges. These fights grant amazing rewards like rare artifacts and ascension materials for weapons and adventurers, but obviously... | Read more »
Moto Rider GO has received a huge update...
Moto Rider GO: Highway Traffic is a popular free-to-play racing game that initially launched back in 2017 and has since racked up over 100 million downloads. Today it has received a sizeable update that introduces several KTM and Husqvarna... | Read more »
ORDESA is a spooky interactive film that...
French studio Cinétévé Experience and ARTE have released interactive movie ORDESA for iOS and Android today. It arrives at the perfect time of year, telling a story about a mysterious haunted house that the viewer suddenly finds themselves lost in... | Read more »
Genshin Impact Guide - How to Beat Storm...
If you've followed our progression guide for Genshin Impact up to Adventure Rank 25, you have reached the point where you can face off against Stormterror on a weekly basis for some pretty sweet rewards. Beating this deadly dragon isn't as easy as... | Read more »
MU Origin 2’s new update welcomes back l...
MU Origin 2 developer Webzen continues to churn out new content for the popular MMORPG and, true to form, it’s just released another update. This one encourages lapsed players to return to the game with the promise of new quests and rewards. [... | Read more »
Genshin Impact Guide - Everything you ne...
Genshin Impact has unveiled its first of what we can expect to be many special events and ongoing content updates. This latest update adds the Elemental Crucible Event. This event is available to any players that have reached Adventure Rank 20,... | Read more »

Price Scanner via MacPrices.net

Apple has 2020 13″ MacBook Airs available sta...
Apple has a full line of Certified Refurbished 2020 13″ MacBook Airs available starting at only $849 and up to $200 off the cost of new Airs. Each MacBook features a new outer case, comes with a... Read more
These major wireless carriers will give you a...
Apple’s wireless partners are offering several deals on iPhone 12 pre-orders right now. If you’re willing to switch carriers, you can get a free iPhone 12 right now. Here’s where to take advantage of... Read more
4 day sale at Sams Club: Save $24-$29 on Appl...
Sams Club has Apple Watch Series 6 GPS models on sale this week for $24-$29 off Apple’s MSRP, starting at $374. Sale ends this Thursday, October 22nd: – 40mm Apple Watch Series 6 GPS: $374.98, save $... Read more
US Cellular offers Apple iPhone 12 Pro for $8...
US Cellular has the 2020 128GB iPhone 12 Pro available for $829 off MSRP for new customers signing up for an Unlimited data plan, or $5.66 per month. Cost of the phone is spread over a 30 month... Read more
Buy one Apple Watch SE or Series 6 at AT&...
Buy one Apple Watch SE or Series 6 at AT&T, and get $200 off the price of a second Apple Watch. One new line required, and price discounted reflected in bill credits over a 30 month period. The... Read more
AT&T offers free iPhone 12, $800 off iPho...
AT&T is offering Apple’s new iPhone 12 for free, or up to $800 off the iPhone 12 Pro, for customers opening a new line of service plus an eligible trade-in. Discount is applied via monthly bill... Read more
US Cellular offers free iPhone 12 for custome...
US Cellular has the 2020 64GB iPhone 12 available for free for new customers signing up for an Unlimited data plan. Cost of the phone is spread over a 30 month period. The fine print: “Promotional... Read more
New Xfinity Mobile promo: Take $250 off Apple...
New customers opening a new line of service can take $250 off the purchase of Apple’s new iPhone 12 or iPhone 23 Pro at Xfinity Mobile through 1/4/21. Service plan required. Their offer reduces the... Read more
New at Verizon: Get the Apple 2020 64GB iPhon...
Verizon is offering the new 2020 64GB iPhone 12 for free for customers switching and opening a new line of service, pre-orders starting at October 16, 2020. They’re offering up to $800 off on 128GB... Read more
Verizon offers the new iPhone 12 Pro for $800...
Verizon is offering the new 2020 iPhone 12 Pro for $800 off MSRP for pre-orders, starting on October 16, 2020. Their offer reduces the price of the 128GB iPhone 12 Pro, for example, to only $199.99... Read more

Jobs Board

Department Manager- Tech Shop/ *Apple* Stor...
…their parents want, and our faculty needs. As a Department Manager in our Tech Shop/ Apple Store you will spend the majority of your time on the sales floor engaging Read more
Geek Squad *Apple* Consultation Professiona...
**782284BR** **Job Title:** Geek Squad Apple Consultation Professional **Job Category:** Store Associates **Store Number or Department:** 000140-San Carlos-Store Read more
*Apple* /Mac IT Support - Randstad (United St...
Apple /Mac IT Support **job details:** + location:San Francisco, CA + salary:$45 - $50 per hour + date posted:Thursday, October 8, 2020 + job type:Contract + Read more
Partner Champion, *Apple* - Insight Network...
Partner Champion, Apple Tempe, AZ, US Requisition Number:78333 As an Apple Partner Champion at Insight, you represent and manage gross profit goals for a valued Read more
Platform - Workplace Eng - *Apple* Enterpri...
MORE ABOUT THIS JOB We are looking for an Apple Platform Engineer who will bring a unique engineering skill set, support, clarity, organization and above all else, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.