TweetFollow Us on Twitter

Delayed Messaging

Volume Number: 14 (1998)
Issue Number: 12
Column Tag: Yellow Box

Delayed Messaging

by Mike Morton

Benefits of Procrastination: Delayed Messaging using the Foundation Kit

Introduction

When you send an Objective-C message to an object, you expect that object to receive the message and process it immediately. Right? Maybe not...

The Foundation Kit provides a way to post messages and have them delivered later, through the method performSelector:withObject:afterDelay:. This method is part of the NSObject class, from which most other classes descend. In this article, we'll discuss not only how you use this method, but give some examples of how delayed messaging solves some difficult problems.

If you've just begun working with Foundation Kit and AppKit, don't worry - we'll explain things step-by-step. In each of the four examples below, we'll describe the problem, show the solution working in the application, and then review the relevant parts of the code. But first, let's look at the method itself.

How to Send a Delayed Message

The method which lets you send delayed messages to any NSObject is declared like this:

- (void) performSelector
	:(SEL) aSelector
	withObject :(id) anArgument
	afterDelay :(NSTimeInterval) delay;

This method takes three arguments:

aSelector specifies the message to send. A selector is sort of the "name" of a method, a very distant cousin of C's function pointers. (In some implementations, a selector is just a "char *" pointer to a unique string value, and this can be useful in debugging, but you should never depend on it being so.) Unlike function pointers, selectors have their own data type, SEL, and you can refer to them in Objective-C code using the @selector(...) construct. For example, if you'd like to send the message setFoo:, you can specify the selector for that message with @selector(setFoo:).

anArgument is an optional object to send with the message. If you don't want to pass anything, you can pass nil. But keep in mind that if you do want to pass something, it must be an object - not an integer or other C type. (In theory, the method specified by aSelector should take a single argument, but no-argument methods seem to work fine. Of course, two-argument methods don't work, because there's no way to pass a second argument with this API.) delay is the number of seconds to delay before sending the message. [If you used the method like this one in earlier releases of Foundation, note that the delay is no longer expressed in milliseconds.] If the program is busy, the message may take longer before being delivered, but it will never get delivered early.

Using this method, you can send any one-argument message with a delay. For example, if you have an object printer which implements the method printString:, you can have it receive a printString: message after a delay of ten seconds with this code:

[printer performSelector :@selector(printString:)
	withObject :@"Hello, world!"
	afterDelay :10.0];

This is almost the same as messaging the object directly with [printer printString :@"Hello, world!"], except that the message gets delivered later, not "while you wait". Also, of course, if printString: returns a value, you can't get that value when using delayed messaging, because you want to continue before the message even gets received. (Modern CPUs are fast, but still don't support time-travel.)

If you change your mind about a delayed message you sent earlier, you can cancel it, using an NSObject class method:

+ (void) cancelPreviousPerformRequestsWithTarget
	:(id) aTarget
	selector :(SEL) aSelector
	object :(id) anArgument;

That's the "how" of delayed messaging. But why would you want to use it. Each of the four examples shows one reason why - let's take a look in more detail. The source code for each of these demos is available online at ftp://ftp.mactech.com.

Example #1: Are They Going to Click Again?

Suppose you're implementing a web browser (just to give Netscape and Microsoft some competition). If the user clicks on a link, the browser displays the new page in the same window. But if they double-click the link, you want to open a new window.

What should your program do on that first click? You can't display the new page (because if a second click shows up, that means the user didn't want to change this window's display). But you also can't ignore the first click (because if no second click arrives, you want to display the new page).

Try it: The demo doesn't browse the web, but it does show an example of not acting on every click. Quickly click on the red rectangle one or more times; it changes color with every click. Now check "Wait for clicks to stop before redisplaying", and it will change color only after you finish clicking.

Figure 1. Acting on multiple clicks

This part of the demo uses a custom view named ColorView, a subclass of the AppKit's important NSView class. The two instance variables for the class remember (1) what color it currently draws, and (2) how quickly it redraws. The class interface, from ColorView.h, begins like this:

Listing 1: Start of ColorView class interface

@interface ColorView : NSView
{
    NSColor *color;             // color we fill with
    BOOL    delaysDisplay;      // YES => wait to redraw
}

The header file also lists access methods, methods which get and set the instance variables, but these aren't shown in Listing 1.

The implementation, in ColorView.m, overrides some methods from NSView - initWithFrame:, which all views use to initialize; drawRect:, which displays the view's contents; and mouseDown:, which handles mouse clicks. The latter two are more important.

The drawRect: method (Listing 2) is simple. It sends a set message to the view's current color, so that subsequent PostScript drawing will use that color, and calls a PostScript function to fill in the rectangle.

Listing 2: ColorView's drawRect: method

- (void)drawRect                // redraw contents of...
    :(NSRect) aRect;            // INPUT: ...this rect
{
    [[self color] set];         // paint with this color
    PSrectfill (aRect.origin.x, aRect.origin.y,
                aRect.size.width, aRect.size.height);
}

The mouseDown: method (Listing 3) is also short, but does a lot more. It updates the view's color, based on the event's click count - the NSEvent method clickCount returns 1 for a click, 2 for a double-click, etc.

Listing 3: ColorView's mouseDown: method

- (void) mouseDown
    :(NSEvent *) theEvent;
{
    //  Update our color: red for a single-click,
    //  orange for a double-click, etc.
    [self _setColorAtIndex :[theEvent clickCount]-1];
}

When mouseDown: sends _setColorAtIndex:, that method winds up calling setColor:, which is where things start to get interesting. When you give the ColorView a new color, it will redraw itself, but it may not do so immediately. After it remembers the new color, it checks "if ([self delaysDisplay])". If it doesn't delay displaying, it sends "[self setNeedsDisplay :YES]" - that's how you ask any NSView to redraw.

But if you click the checkbox "Wait for clicks to stop before redisplaying", the checkbox sends takeDelaysDisplayFrom: to the ColorView, which will take a new value of delaysDisplay from the checkbox.

And when [self delaysDisplay] returns YES, the ColorView knows that it shouldn't redraw, but it also that it shouldn't forget about the color change, since it needs to redraw at some point. So it does the code in Listing 4: first, it removes any pending redraw requests, then it sends a _setNeedsDisplayYes message, but delaying the delivery. (Why doesn't it send a delayed setNeedsDisplay:, with an argument of YES? Because the argument of a delayed message must be an object.)

Listing 4: ColorView's delaying of redraw

{
        //  Cancel any pending message...
        [NSObject cancelPreviousPerformRequestsWithTarget
                        :self
               selector :@selector(_setNeedsDisplayYes)
                 object :nil];
        
        //  ...and queue a new one
        [self performSelector
                        :@selector(_setNeedsDisplayYes)
             withObject :nil
             afterDelay :REDRAW_DELAY];
}

How on earth does this work? Every time you click the ColorView, it remembers a new color, and posts a delayed message to redisplay itself soon. If you click again, soon enough, it cancels that delayed message and posts another, and so on. When you stop clicking, the last message you posted will get delivered, and the view redraws just once.

It's sort of like being a painter in a hotel with a finicky owner. Every few minutes, the owner changes their mind about what color they want. So you call the front desk, and say "Wake me in an hour". If the owner wakes you up early to change the color again, you phone the front desk and say "Cancel that wake-up call, and wake me an hour from now". Only after the owner goes a full hour without changing their mind will you get a wake-up call (a delayed message) - and then it's time to paint.

Without delayed messaging, it would be very hard to "predict" whether a mouse click will be followed by another. With the ability to post a message for future delivery, it takes just a few lines of code.

Example #2: Are They Done Typing?

The world is full of query user interfaces in which you type what you want to find, then press Return (or click "OK" or "Find" or "Search" or...). But if doing a search doesn't take a lot of time, why make the user press return? Why not just search when the users pauses in typing?

Try it: Type one or two letters in the box labeled "Find:". A moment after you finish typing, the "Items found" scrolling list will change to list only entries containing what you typed. Type some more to refine the search, or delete characters to expand it again. Adjust the "Wait for pause of..." field to find a delay which works well for you.

Figure 2. Automatic querying.

This solution works much like the previous one, but instead of waiting for a pause after one or more mouse clicks, you're waiting for a pause after one or more keystrokes (or other changes).

We can't just override keyDown: like we did for mouseDown: in the last example, because the field can change for things other than keystrokes (such as by pasting or cutting text). But it's easy to monitor an NSTextField for changes: we make the application controller be the delegate of the field. (You can see this relationship by inspecting the field's connections in the nib file.)

Each time the user changes the field, the controller receives a controlTextDidChange: message. [NSTextField documentation in the DR2 release promises a textDidChange:, but this seems not to get sent, and NSControl documentation says it's controlTextDidChange:.] The application controller's implementation is in Listing 5. It wipes out the results of any previous search, cancels any previous, delayed reload messages, and sends a delayed reload message to the table.

Listing 5: Handling a change in a query field

- (void) controlTextDidChange
    :(NSNotification *) notification; // IGNORED
{
    //  Wipe out any cached list of filtered objects.
    [self setFilteredStrings : nil];

    //  Cancel any pending message.
    [NSObject cancelPreviousPerformRequestsWithTarget
                :findResults
       selector :@selector(reloadData)
         object :nil];

    //  Now tell the table to reload after the a delay.
    //  (When it reloads, it'll get a new list of strings.)
    [findResults performSelector :@selector(reloadData)
               withObject :nil
               afterDelay :[findThreshhold floatValue]
                            /1000.0];
}

Let's skip the details of how reload redisplays the table to show only items matching your search string, but - in brief - it goes like this:

  • the table gets the delayed reloadData
  • the table sends numberOfRowsInTableView: to the controller
  • the controller computes [self filteredStrings]
  • filteredStrings finds and saves matching strings
  • the table sends tableView:objectValueForTableColumn: to the controller
  • the controller uses the filtered strings to find the object value

One other thing to note: the method awakeFromNib also sends reloadData to the table, which makes the table display its contents when the application starts, before you type anything.

Example #3: Did They Let Go of that Slider Yet?

All controls give you a choice of when you'd like them to send you their action message. For a button, you can ask to get the message continuously, as long they hold the mouse down in the button (useful for a scroller's arrow) or only when they release the button (useful for an "OK" button). You can set which way a control behaves with the setContinuous: method.

Suppose your 3-D drawing application has a slider used to set the viewing angle. While they're dragging the slider, you want to quickly draw a wire-frame image to give them an idea of what it'll look like. When they release the mouse, you want to redraw a full image.

But the slider sends only one type of action message, during dragging or when you release. How can you act on both"

If you want to do it the hard way, you could subclass NSSlider to support two types of action messages or to tell you if it's currently tracking. If you want to do it the elegant way, you could ask the shared NSApplication instance for currentEvent, which is the last event processed, and ask what kind of event provoked the action message. But this is an article about delayed messaging, so we'll do it the fun way.

Try it: The demo doesn't do wire-frame drawing, but as you drag the slider it does show you the "Sliding" value, then when you release the mouse, it'll show you the "Final" value.

Figure 3. Distinguishing dragging from mouse-up

This solution is a little tricky, and illustrates something about when delayed messages are actually delivered. As we've said before, a delayed message will never get delivered early, but it may get delivered late. This is because the application processes delayed messages only when it's idle. During the time you're dragging the mouse, the application is busy tracking the drag, and doesn't check if it needs to deliver delayed messages. So you can post a delayed message which says "the mouse went up", trusting that it won't get released until the user releases the mouse, and the control finishes tracking. (Nothing in the documentation promises this will work, so don't use this idea in a production application. But for demo purposes, it's fine.)

Listing 6 shows the implementation is simple: The slider sends mouseUpSliderAction: when you move it, and this method will:

  1. update the "Sliding" value in the UI
  2. wipe out any left-over "Final" value in the UI
  3. post a delayed _sliderFinished if it hasn't done so already

Listing 6: The action method for the slider

- (IBAction) mouseUpSliderAction :sender;
{
    //  Display "sliding" value, and clear "final" value
    [mouseUpSlidingValue setIntValue
        :[mouseUpSlider intValue]];
    [mouseUpFinalValue setStringValue :@"--"];

    //  If this is our first time getting this message
    //  since they began dragging, we want to remember
    //  to do something when they release the mouse, too.
    //  If we haven't queued that reminder yet, do it now.
    if (! mouseUpMessagePending)
    {
        [self performSelector :@selector(_sliderFinished)
                   withObject :nil
                   afterDelay :0];
        mouseUpMessagePending = YES;
    }
}

As you might guess, the _sliderFinished method clears the "Sliding" value, sets the "Final" value, and clears the flag. That's all there is to it.

Example #4: Simple Animation

You're almost done with that prototype which you want to sell to Microsoft, and it's got everything a Microsoft product should have: a complex UI, incompatible functionality, too many features, bugs galore... what else could Microsoft want?

Then you remember: They want a little assistant in the corner of the screen, animated to scratch or lick itself when you're not doing much. How can you animate when you don't have a real-time system - and how can you make sure the animation doesn't slow things down when the user is working?

If you've read this far, I hope you've guess the answer by now: you can do animation with delayed messaging.

Try it: Click "Run" to start the selection running around the matrix. Try typing different animation rates into the "Animate ... frames/second" field - you can type a new value while the animation is running.

(Now try dragging the slider (from example 3). The animation doesn't run while you're dragging. Again, delayed messages don't get delivered at all during certain operations.)

Figure 4. Simple animation

The code to implement this depends on one state variable, animationDirection, which keeps track of which way the selection is moving in the matrix. Clicking "Run" sends the animationRun: message, which just makes sure the direction is initialized, then sends _doAnimation to do the first "frame".

The _doAnimation method (shown in Listing 7) draws one "frame" by finding out what cell is selected and selecting the next one, reversing direction if it's reached the last cell. Then it calculates the current inter-frame delay from the text field, and queues a message to do itself again after that delay.

Listing 7: Drawing one "frame" of animation

- (void) _doAnimation;
{
   int		tag;
   float	delayInSeconds;

   //  Get current position; bump it in current direction
   tag = [animationMatrix selectedTag];
   tag += animationDirection;

   //  Cheap hack: If the tag runs off the end,
   //  there's no cell with that tag.
   if ([animationMatrix cellWithTag :tag] == nil)
   {
       //  Whoops! Switch direction and head the other way.
       animationDirection = -animationDirection;
       tag += (2*animationDirection);
   }

   //  "Animate" to the new position. Because it's a set
   //  of radio buttons, we needn't deselect the old cell.
   [animationMatrix selectCellWithTag :tag];

   //  Figure the delay, which may have changed if they
   //  typed a new number, and queue a message to
   //  do all this again.
   delayInSeconds = [animationDelay floatValue]/1000.0;
   [self performSelector :_cmd
              withObject :nil
              afterDelay :delayInSeconds];
}

Instead of using @selector(_doAnimation) as the selector, I chose to pass just _cmd. This obscure variable is automatically declared in every Objective-C method; it refers to the method currently being executed. Using it instead of the @selector(...) construct is largely a stylistic choice - I wanted to make it clear that the method is invoking itself.

But... wait a minute! If a method invokes itself unconditionally, doesn't that cause infinite recursion? Usually, yes. But in this case, the invocation doing the calling returns before it gets called again. So each invocation of the method happens independently of the previous ones - and without the previous ones on the stack.

So it's not recursive, but just like in a correct recursive algorithm, there is an end to the succession of invocations. If you click the "Stop" button, the animationStop: method will use cancelPreviousPerformRequestWithTarget:selector:object: to cancel the currently-pending invocation, breaking the chain and stopping the animation.

The Foundation Kit's NSTimer class provides another convenient way to do animation by repeatedly sending a message for you at fixed intervals. I chose delayed messaging here because it makes it easy to change the animation rate: note how _doAnimation immediately reacts to a new rate in the field, while it would be a little more complicated with a timer.

Again, keep in mind that delayed messages get performed only when the application is idle. You can have multiple animations running, but they'll take up no time during intensive computations. This can be useful if your UI needs to flash to show the user important conditions, but doesn't want to slow things down.

Other Applications

  • If updating an inspector or ruler slows your application, send it a delayed message. If the user changes the selection again soon, the inspector or ruler can avoid an unnecessary update.
  • You can minimize propagation in a two-level network. If multiple objects can trigger some reaction (such as recalculating) in a single target object, they can all send delayed messages with a zero delay, cancelling previous messages, and the target will receive just one message.
  • You can make an app terminate after a given period of inactivity by sending a delayed terminate: message to the application, then cancelling and re-posting the message each time there's some activity.
  • ...watch this space! I'd like to hear of uses you find for delayed messaging.

Conclusion

Delayed messaging is a simple Foundation Kit feature which can help solve some challenging problems. Keep the technique in mind, and you'll find interesting applications for these and other techniques.

Acknowledgements

Thanks to Art Isbell and Byron Han for their comments and suggestions. Thanks also to Lee Worden, for his help and ideas on an earlier version of this article, written for the first release of the Foundation Kit.

Further Reading

In addition to the NSObject class description, and its brief description of delayed messaging, you might find the following interesting:

NSObject class documentation
In addition to providing the authoritative word on the methods describes above, this also explains performSelector:withObject:afterDelay:inModes:, which gives you finer control over how idle the application has to be before the message gets sent.

NSTimer and NSInvocation class documentation
Delayed messaging uses concepts from timers and invocations. While you need not understand either of these classes to use it, you might find the details enlightening.

Apple's documentation for Enterprise Objects Framework (EOF)
EOF uses delayed messaging internally in a variety of ways to improve efficiency. For example, certain notifications are delayed so redundant notifications can be coalesced into one.

Ben Shneiderman, Dynamic Queries for Visual Information Seeking, originally published in IEEE Software 11, 6 (November 1994), 70-77, available at ftp://ftp.cs.umd.edu/pub/hcil/Reports-Abstracts-Bibliography/93-01html/3022.html. This article argues out that hardware has become so fast that you can evaluate queries instantly (without waiting for the user to press Return or click "OK" or...), and - unlike so many articles on GUIs - has a number of figures illustrating specific GUI ideas.


Mike Morton, mike@mikemorton.com, spent seven years developing software for Macintosh, then seven years developing for NextStep and OpenStep. (You may draw your own parallels to biblical dreams of seven lean years and seven fat years.) He currently works for Apple Enterprise Technical Support, which old-timers know as "NeXT Tech Support".

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

NeoFinder 7.4.1 - Catalog your external...
NeoFinder (formerly CDFinder) rapidly organizes your data, either on external or internal disks, or any other volumes. It catalogs and manages all your data, so you stay in control of your data... Read more
Tunnelblick 3.8.0 - GUI for OpenVPN.
Tunnelblick is a free, open source graphic user interface for OpenVPN on OS X. It provides easy control of OpenVPN client and/or server connections. It comes as a ready-to-use application with all... Read more
Tweetbot 3 for Twitter 3.3.1 - Popular T...
Tweetbot is a full-featured OS X Twitter client with a lot of personality. Whether it's the meticulously-crafted interface, sounds and animation, or features like multiple timelines and column views... Read more
Monosnap 3.5.9 - Versatile screenshot ut...
Monosnap lets you capture screenshots, share files, and record video and .gifs. Features Capture Capture full screen, just part of the screen, or a selected window Make your crop area pixel... Read more
Monosnap 3.5.9 - Versatile screenshot ut...
Monosnap lets you capture screenshots, share files, and record video and .gifs. Features Capture Capture full screen, just part of the screen, or a selected window Make your crop area pixel... Read more
Tweetbot 3 for Twitter 3.3.1 - Popular T...
Tweetbot is a full-featured OS X Twitter client with a lot of personality. Whether it's the meticulously-crafted interface, sounds and animation, or features like multiple timelines and column views... Read more
Tunnelblick 3.8.0 - GUI for OpenVPN.
Tunnelblick is a free, open source graphic user interface for OpenVPN on OS X. It provides easy control of OpenVPN client and/or server connections. It comes as a ready-to-use application with all... Read more
Bookends 13.2.5 - Reference management a...
Bookends is a full-featured bibliography/reference and information-management system for students and professionals. Bookends uses the cloud to sync reference libraries on all the Macs you use.... Read more
Quicken 2019 5.11.2 - Complete personal...
Quicken makes managing your money easier than ever. Whether paying bills, upgrading from Windows, enjoying more reliable downloads, or getting expert product help, Quicken's new and improved features... Read more
Bookends 13.2.5 - Reference management a...
Bookends is a full-featured bibliography/reference and information-management system for students and professionals. Bookends uses the cloud to sync reference libraries on all the Macs you use.... Read more

Latest Forum Discussions

See All

Void Tyrant guide - Tips and tricks for...
Void Tyrant continues to get a lot of play in these parts. Probably because the game is just so deep and varied. The next stop on our guide series for Void Tyrant is class-specific guides. First up is the Knight, as it’s the first class anyone has... | Read more »
Summon beasts and battle evil in epic re...
Imagine a tale of conlict between factions of good and evil, where rogueish heroes summon beasts to aid them in them in warfare and courageously battle dragons over fields of scorched earth and brimstone - that's exactly the essence of epic fantasy... | Read more »
Upcoming visual novel Arranged shines a...
If you’re in the market for a new type of visual novel designed to inform and make you think deeply about its subject matter, then Arranged by Kabuk Games could be exactly what you’re looking for. It’s a wholly unique take on marital traditions in... | Read more »
TEPPEN guide - The three best decks in T...
TEPPEN’s unique take on the collectible card game genre is exciting. It’s just over a week old, but that isn’t stopping lots of folks from speculating about the long-term viability of the game, as well as changes and additions that will happen over... | Read more »
Intergalactic puzzler Silly Memory serve...
Recently released matching puzzler Silly Memory is helping its fans with their intergalactic journeys this month with some very special offers on in-app purchases. In case you missed it, Silly Memory is the debut title of French based indie... | Read more »
TEPPEN guide - Tips and tricks for new p...
TEPPEN is a wild game that nobody asked for, but I’m sure glad it exists. Who would’ve thought that a CCG featuring Capcom characters could be so cool and weird? In case you’re not completely sure what TEPPEN is, make sure to check out our review... | Read more »
Dr. Mario World guide - Other games that...
We now live in a post-Dr. Mario World world, and I gotta say, things don’t feel too different. Nintendo continues to squirt out bad games on phones, causing all but the most stalwart fans of mobile games to question why they even bother... | Read more »
Strategy RPG Brown Dust introduces its b...
Epic turn-based RPG Brown Dust is set to turn 500 days old next week, and to celebrate, Neowiz has just unveiled its biggest and most exciting update yet, offering a host of new rewards, increased gacha rates, and a brand new feature that will... | Read more »
Dr. Mario World is yet another disappoin...
As soon as I booted up Dr. Mario World, I knew I wasn’t going to have fun with it. Nintendo’s record on phones thus far has been pretty spotty, with things trending downward as of late. [Read more] | Read more »
Retro Space Shooter P.3 is now available...
Shoot-em-ups tend to be a dime a dozen on the App Store, but every so often you come across one gem that aims to shake up the genre in a unique way. Developer Devjgame’s P.3 is the latest game seeking to do so this, working as a love letter to the... | Read more »

Price Scanner via MacPrices.net

Apple’s $1489 clearance price on refurbished...
Apple has Certified Refurbished 2018 13″ 2.3GHz 4-Core Touch Bar MacBook Pros available starting at $1489. Apple’s one-year warranty is included, shipping is free, and each MacBook has a new outer... Read more
New 2019 13″ 2.4GHz 4-Core MacBook Pros on sa...
Apple resellers B&H Photo and Amazon are offering the new 2019 13″ 2.4GHz 4-Core Touch Bar MacBook Pros for $150 off Apple’s MSRP. These are the same MacBook Pros sold by Apple in its retail and... Read more
B&H drops prices another $50 on clearance...
B&H Photo has dropped prices on clearance 2018 13″ MacBook Airs by a further $50 with models now available for $250 off Apple’s original MSRP. Overnight shipping, or expedited shipping, is free... Read more
Find the best sales & lowest prices on Ap...
Our Apple award-winning price trackers are the best place to look for the best sales and lowest prices on Apple gear. Scan our price trackers for the latest information on sales, bundles, and... Read more
Apple has clearance 2018 13″ MacBook Airs now...
Apple has Certified Refurbished 2018 13″ MacBook Airs available starting at only $849. Each MacBook features a new outer case, comes with a standard Apple one-year warranty, and is shipped free. The... Read more
Save $400 on the 8-Core iMac Pro today at Ama...
Amazon has the base 8-core iMac Pro on sale today for $4599 including free shipping. Their price is $400 off Apple’s MSRP, and it’s the currently lowest price available for an iMac Pro. For the... Read more
Flash sale! New 11″ 1TB WiFi iPad Pros for th...
Amazon has the 11″ 1TB WiFi iPad Pro on sale today for only $1199.99 including free shipping. Their price is $350 off Apple’s MSRP for this model, and it’s the lowest price ever for a 1TB 11″ iPad... Read more
Weekend Deal: 2018 13″ MacBook Airs starting...
B&H Photo has clearance 2018 13″ MacBook Airs available starting at only $999 with all models now available for $200 off Apple’s original MSRP. Overnight shipping, or expedited shipping, is free... Read more
Apple has clearance 10.5″ iPad Pros available...
Apple has Certified Refurbished 2017 10.5″ iPad Pros available starting at $469. An Apple one-year warranty is included with each iPad, outer shells are new, and shipping is free: – 64GB 10″ iPad Pro... Read more
Apple restocks refurbished iPad mini 4 models...
Apple has restocked Certified Refurbished 32GB iPad mini 4 WiFi models for $229 shipped. That’s $70 off original MSRP for the iPad mini 4. Space Gray, Silver, and Gold colors are available. Read more

Jobs Board

Best Buy *Apple* Computing Master - Best Bu...
**711495BR** **Job Title:** Best Buy Apple Computing Master **Job Category:** Store Associates **Location Number:** 000882-Waterfront-Store **Job Description:** The Read more
Best Buy *Apple* Computing Master - Best Bu...
**711597BR** **Job Title:** Best Buy Apple Computing Master **Job Category:** Sales **Location Number:** 000873-Colma CA-Store **Job Description:** **What does a Read more
Best Buy *Apple* Computing Master - Best Bu...
**711346BR** **Job Title:** Best Buy Apple Computing Master **Job Category:** Store Associates **Location Number:** 001095-Chesterfield-Store **Job Description:** Read more
Best Buy *Apple* Computing Master - Best Bu...
**704899BR** **Job Title:** Best Buy Apple Computing Master **Job Category:** Sales **Location Number:** 000135-Pleasant Hill-Store **Job Description:** **What does Read more
Best Buy *Apple* Computing Master - Best Bu...
**707083BR** **Job Title:** Best Buy Apple Computing Master **Job Category:** Sales **Location Number:** 000045-Rockford-Store **Job Description:** **What does a Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.